{
  "video_id": "e_MO-Rcz_MA",
  "title": "Build and Deploy a Full Stack ElevenLabs Clone with Next.js 16",
  "url": "https://www.youtube.com/watch?v=e_MO-Rcz_MA",
  "transcript": "Welcome to the future of voice generation. That voice you just heard isn't a real recording. I cloned my own voice and generated that entirely with AI. And this is the platform you are going to build. Meet Resonance, a full-stack AI voice generation platform that you are going to build from scratch. User authentication, team workspaces, custom voice creation, usage-based billing. The whole thing. But here's what makes this project different from anything else out there. The AI model that generates the speech isn't some paid third-party API. You will learn how to self-host Chatterbox text-to-speech model on a serverless GPU. You will own the entire voice generation pipeline. Here's the full flow. Pick a voice. Dial in the generation settings, creativity, voice variety, expression range, natural flow. These aren't decorative sliders. They map directly to the model's inference parameters. So you decide exactly how expressive or consistent the output sounds. Hit generate and the audio comes back into a fully interactive player. Listen up, adventurer. The realm of Ashenval is crumbling. Scrap through the waveform, skip forward and back, download the file, all right from the browser. Storage is S3 compatible. So Cloudflare R2, AWS S3, MinIO, whatever fits your stack. And every generation is automatically metered against the organization's billing account using Polar. Users can create custom voices in two ways. Upload an audio file with drag and drop or record one straight from the browser. Hit record and you get real-time visual feedback of what the microphone is picking up. Stop, preview your recording and then fill in the metadata. name, category, and language. The voice gets stored and is immediately available for your entire team to use in their own text-to-speech generations. Your custom voice is now live. It shows up on the voices page under your team's collection, where you can preview it or jump straight into generating with it. Every generation is saved in the history tab, so nothing gets lost. Now here's how you can actually monetize this. Polar powers usage-based billing per organization. You will learn how to charge for each voice creation, as well as for each character generated, and how to adjust pricing to cover your infrastructure costs and generate real revenue. Here's how everything works together. User input flows through a type-safe API layer to the Chatterbox text-to-speech model. Self-hosted, so you only pay for GPU time when someone actually generates speech. Audio is stored in Cloudflare R2 and streamed back through an optimized proxy. Clerk manages authentication and team workspaces. Each organization gets its own isolated voices and usage. Prisma handles the data layer, the ORM for typed queries and migrations, paired with Prisma Postgres as the database for an optimized end-to-end experience. Polar tracks every generation and builds each organization based on your pricing tiers. Railway handles deployment. No cold starts, no surprise serverless builds, no AWS wrapper adding markup. They run on their own hardware. Push to GitHub and your app is live. Every pull request gets its own preview environment, so nothing hits production untested. Sentry handles error monitoring, bringing the whole project one step closer to a production-ready app. And CodeRabbit reviews every pull request in this build. During the build of this project, it caught bugs I completely overlooked that would have shipped a broken app to production. That is Resonance, a real SaaS product built step by step. Every tool used in this project offers a free tier, so you can build the entire thing without spending a cent. The full source code is also free and available. Link is in the description. And now, let's get started. Before we dive in, using the link on the screen, you can get three months of Sentry Team completely for free. If that sounds useful for your project, feel free to grab the deal. And now, let's build. In this chapter, we're going to set up the entire project foundation, a Next.js 16 app with Tailwind version 4 and ShadCN UI component library, clerk authentication with organization support, making our application multi-tenant by default. And finally, we're going to add a Postgres database with Prisma ORM. By the end of this chapter, you will have a working application with authentication, multi-tenancy, and database models. Let's get started by confirming we have the prerequisites to start building. Head inside of your terminal and make sure that your node version is above 20.8. If you have a version lower than 20.8, you need to upgrade before you continue this tutorial. As per the other commands, it's not exactly important what version you have because they can be different depending on your operating system. Just make sure you're not getting any errors from running these three commands. Once we've established that we have the prerequisites, we can go ahead and run the next.js setup. So using their documentation page, we can find npx create next app as the quick start command. But we're going to do a slight modification. So instead of running at latest, we're going to specify a version 16.1.6. The reason we are doing this is so that it's easier for you to follow this tutorial. Depending on when you arrive on this tutorial, perhaps the newest latest version is a breaking change, making it that much harder for you to follow along. So in order to avoid those headaches, I recommend using the same version as me. And then once you complete the tutorial, give yourself a challenge to upgrade to the latest version. Once we define the version, we need to give our project a name. This can be whatever you want. I'm going to call it resonance. Let's go ahead and check the preferences. So I'm going to go down to customize settings so we can select our own preferences. We're going to select yes for TypeScript. We're going to choose slint for the linter. We're going to select no for React compiler. Yes for tailwind. And this one is important because more often than not the default option here is no. Use the arrow key to change it to yes. We will be using the source directory and make sure you have yes selected for the app router as well. I'm not going to change the import alias so I'm going to leave that to be no and let's give it a minute to install. Once the app has been installed you will see a success message like this. Let's go ahead and change directory to be inside of that new project. So change directory to your project name. And go ahead and run ls command so you can see the output. If you see something like this or more specifically exactly like this, you are in the correct repository. This is important because the terminal will be the place where we're going to execute some commands. So it's important that we are in the right repository which we've just established. Great. Now let's go ahead and do the same thing in our editor. For your editor, you can use whatever you want. I'm going to use a popular one called VS Code. If you want to use cursor, Z, Nvim, it's perfectly fine. So let's just go ahead and open and select our project resonance. And I'm just going to close all of these tabs here. And what we're going to do now is just confirm that we have the same file structure. So make sure you have the source folder and inside, make sure you have the app folder. if you have those two you are practically done with confirming the entire file structure because those are the two most important folders to have the second one i would like to confirm is actually inside of package.json so in here just go ahead and confirm your next version once again 16.1.6 of course if you want to use a higher version than this it's perfectly fine but as i said if you have a different major version like 17 or 18, I can't guarantee there won't be any breaking changes from the code I will be writing in this tutorial. So if you are on 16.1.7 or 8, it probably doesn't matter. Even if you are on this one like 16.2, again, I don't think it matters that much. But major versions, these ones might be a problem. Great. And another important thing is to confirm you have tailwind added. So we are using tailwind version four. Perfect. So once we have that established, we can go ahead and run shatcn init. So before we run this command, I think it's fair to go ahead and learn what shatcn is. So shatcn is not exactly a component library. It's more of a foundation for your design system. So it's a set of beautifully designed components that you can customize, extend, and build on. I'm going to go ahead and go through the documentation here. And I will go through pick your framework, Next.js, and select NPM. And here it is. NPX Shatzian at latest init. And I'm going to do the same thing that I did before. instead of using latest I will first check what is the latest version That what interests me So 3 This way if you want to you can use the exact same version as me So let go ahead and make sure you are inside of the resonance project And let's just run in it. What this is going to do is, first of all, it's going to install the package. And then it will simply ask us a few questions. It's going to recognize all the files automatically. so you don't have to worry about that. You can see it found Next.js, it found Tailwind version 4, it validated the import alias. Now for the base color, I'm just going to choose neutral and that's it. You can see it recognizes our globals.css file, it's going to do some updates, it's going to install some packages. So let's go ahead and have our app open here. So we can see the changes, right? So what are the changes right now? You can see that we have some new packages installed. Let me go ahead and expand this. So we have class variance authority, CLSX, Lucid React, Radix, and Tailwind Merge, as well as ShatCN and Tailwind Animate CSS. So all of these were added by ShatCN. additionally globals.css was completely reworked and finally we also have components.json components.json is basically a configuration file of the options we just selected the base color our alias the icon library and all of that basically not something we're gonna manually modify. I just want to make you aware of your code changes. And we also have the utils folder. The utils folder, as you can see right here, consists of a singular CN function, which we're going to heavily rely on later in the project. It's basically a way to dynamically write Tailwind classes. All right. So that is ShadCN. So before we actually do anything, let's go ahead and run chat cn 3.8.5 add button what this is going to do is it's going to install one of those foundational components inside of source components ui button so app folder my apologies source components ui should now have a button and the cool thing is you actually have the entire source code instead of it being hidden inside of node modules this makes ai agents able to work with it, you able to work with it, basically a perfect foundation system. Let's go inside of our app folder, page.tsx. And what I like to do here is I like to clear the entire thing. And let's render that button and let's right click me. We can remove the image since we're not using it. So page is kind of like the root file of this project. We are going to work with layout, but in a moment. For now, let's just focus on app folder page. Save this file and let's go ahead and do npm run dev. And you will have your app running on localhost 3000. So let me just go ahead and have that ready here. And just like that, you should have click me button right here. and you by default have a couple of variants like destructive, ghost, outline and you even have some sizes like large, small or even extra small and the cool thing is you can always command click to go inside of the source code it's located inside of components UI button and in here if you want to you can add your own size inside of this object right or you can add your own variant here so if I go ahead and add purple background purple 500 and text white you can see that immediately here I have purple available and just like that I've added my own variants. So that's what they mean by foundational library. You will be able to customize it to fit your design. So now I'm just going to remove it from here. I just wanted to demonstrate to you how it works. Great. So once we have that, what I like to do is I like to add all of the components immediately. You can do that by running a Shazian 3.8.5 add dash dash all. We're not going to need all of them but i simply think it's easier to work by having all of them available and then cleaning up later especially if we are going to have to remember the version 3.8.5 during the entire tutorial i think is just easier to add all the components uh right now uh so you can see that it skipped one file which is button simply because we already added it uh and in here it's a general warning that a tooltip component has been added and if you want to use it you need to wrap your project within a tooltip provider so that's something to remember if we plan on using that and by now you should have a lot of new files here so those are all of the components you can now use besides that you will also see a hook called use mobile which will come in handy it's kind of like a media query responsive hook. Very useful. We are going to use that definitely. All right. So once we've done that, we initialized chat CN and we added all components. Now we have to do some key changes here. So I'm going to go ahead and do npm run dev simply so we have our app running. and right now the only thing we see here is a button called click me so i'm just going to do some changes in the layout file which are mostly just you know design changes for example in here we have two fonts defined and just by looking at this return you can kind of deduct what the this root layout file is. It's basically the entire app parent, I guess you could call it container, which encapsulates the app with HTML and the body and then renders the first page inside 30 children. That's called the root layout and it needs to exist in every Next.js app. You can see in here we have some reserved constant exports for metadata, which give our page a title and description. And here is also where we define the font. So I'm going to go ahead and change from gaze sounds to enter. So let's go ahead and import enter from next font Google. And let's go ahead and change this to be enter. And let's go ahead and use enter dot variable. So this is a very subtle font change simply because at the moment, we don't really have too many text elements here. So that is the first key change that we have added. So enter font. And now I also want to do another thing inside of the layout while we are here so I don't forget. And that is to import toaster. You can import toaster from components UI Sonar. The reason I'm doing that now is so I don't forget it later. This basically allows us to fire toasts. And we can even try it right now. Well, let's go ahead and just mark this as use client, which enables this app to be reactive and give this button on click elements. And let's import toast from Sonar package. And let's just do toast.success hello world. So now when you click on click me, you will see hello world right here. Let me expand the screen so you can see it a little bit better. click me there we go perhaps i need to zoom out even more there we go hello world so if you forget to add sonar in your layout i mean the toaster right if you comment it out you can see that toasts are not appearing so since we're going to be using uh sonar a lot that's why i decided to added right now. So components UI Sonar in layouts and in page it's just the Sonar package. All right so we just finished the layout.tsx. Now another thing I want to do is I want to change the use mobile hook. So we briefly mentioned this you now have a hooks folder use mobile all of this was added by Shatsy NUI and the mobile breakpoint here is 768. I'm going to change it to 1024 simply because that is the breakpoint we are going to be working with. So just go ahead and change this to 1024. Great. One last change I want to do is globals.css. So you don't have to do this if you don't want to, the only thing this will do is it will change the look of your app, right? Ever so subtly, right? It's not going to be anything too drastic. The problem is we're not going to write this by hand. Using the link on the screen, you can access the source code of this project. And once you are in the source code, go inside of source app folder globals.css and simply copy my file, delete yours, and paste the new one inside. So this is a completely new one that I have just added here. You can verify by this. If you have this button not disabled, you have correctly copied and pasted myglobals.css from the source code. Again, link on the screen so you can access it. What this does is you can't exactly see it now, but there are some subtle differences. You can see the buttons are a bit more rounded. If I kind of command Z, you can see how the previous one didn't have as rounded ones. So once you copy myglobals.css and save, you will see the buttons are now more rounded. So it a bunch of subtle changes like that For example the buttons now also have a pointer cursor whereas the previous version the default one doesn right So I basically made that file that you can copy and paste here Obviously, you don't have to write this by hand. You won't even learn anything by doing so. So you can have the exact same look and feel as me. If for whatever reason you're having trouble with this, you really shouldn't because it's open source. You can see the link on the screen and just copy globals.css and paste it in your project. You don't have to do it. It's not going to change the functionality of the project at all. All right. So those are the three key changes. Global CSS, layout.tsx, and use mobile. NPM run dev works. Interfont is rendering, and Shadzian components are importable. Perfect. So what we're going to do now is we're going to protect our app with authentication so let's start by running npm install clerk forward slash next js npm install at clerk forward slash next js let me just confirm that there we go uh and once that is installed we're gonna go ahead uh and set up our app so what i want to start with is the layout file once again So let's go inside of layout.tsx and let's go ahead and import clerk provider from our newly installed package clerk next js. And let's go ahead and wrap the entire app around the clerk provider. I mean, let's wrap the clerk provider around the entire app like that. Then let's go ahead and make sure we have our app running. and let me go ahead and refresh all right and in here you can see that clerk has now entered keyless mode so we briefly saw that error about not having keys so let's go ahead and just add keys so we can immediately claim this application or if you are getting that error and you can't get rid of it, using the link on the screen, you can visit clerk.com. So let's go ahead and sign in and enter the dashboard. So in this dashboard, I already have some projects. If you don't have any, you will most likely just see this startup screen. So I'm going to call this project Resonance. I will select the email provider and the Google provider. but you can see you have many more options so feel free to select the ones that make the most sense for your project let's go ahead and hit create application let's go ahead and make sure next js is selected here so we already installed next js so we don't have to do that the only thing we have to do is copy the keys so i'm just going to go ahead and copy these and then we're going to go ahead in the root of our app. And we're going to create a new file dot environment. And just paste next public clerk publishable key and clerk secret key. One important thing is that this dot environment file is if you're using VS code, it should be kind of grayed out. And in your gitignore, you should very clearly see .environment file as ignored. So it's never committed to your GitHub repository because these are important secrets here. And your gitignore is also now updated to feature the clerk configuration. So that's a hidden folder. And inside of here, it simply entered the keyless mode. And now once you've done that, let's go ahead and refresh the app again and you can see we no longer have that warning here. So not much is happening right now because we don't really have anything established yet, right? We don't have any rules made. So we have to go ahead and create a middleware. In previous versions this was called middleware.ts but in next js 16 and onwards it's been renamed to proxy which actually more accurately describes what this file is just make sure you create it within the source folder if it's outside of the source folder it's not going to work so in here i'm just going to go ahead and import the following following two functions from clerk next js server that's clerk middleware and create route matcher then i'm gonna go ahead and establish the public routes using create route matcher the public routes basically means anyone can visit these routes both logged in and logged out fox well actually only logged out people should be able to visit these right and then i'm just going to add another one which is the organization select selection route which will be forward slash organization selection if you're wondering what are these parentheses and asterisks it's basically just to cover all the additional routes after that and now in here we have to go ahead and export the actual clerk middleware. What we're going to do here is we're going to extract user ID and organization ID from ALF. Make sure to await it. And the first thing we're going to do is we're going to allow public routes. So we also have to import next response. My apologies. We can do that from next server. So if the user hits a public route, which is either sign in or sign up, let's just go ahead and continue with our work. Then let's go ahead and let's protect non-public routes. So if we don't have user ID, let's trigger alf.protect, which is just going to do its job. And then let's go ahead and allow an organization select route. So if the user is logged in, and it attempts to visit organization select route, let's make sure we allow them to do so. and now let's do the following for every single protected route and all of those protected routes are basically any route besides this one this one and this one right so we are using reverse logic here so we don't have to manually specify which ones are protected all of them are protected besides sign in sign up and organization selection so if the user attempts to visit any of those protected apps, we need to make sure that they have an organization selected. So if the user is logged in, but the user doesn't have organization selected, we have to go ahead and redirect the user to our org selection. So make sure you use the new URL constructor here, like this. And then let's use the next response dot the redirect and redirect them to the org selection and finally let's go ahead and return next response dot next now what we have to do here is we have to add this matcher and i know how did i get this where did i get this confusing line don't worry you can find this in the actual clerk middleware proxy here so we were just following these clerk API keys. And in here, you can see proxy.ds. You can see this one is simpler. The only thing that actually need to copy here is export const config. You can copy this entire thing. That's where I got it from. So just go ahead and paste it. Great. And you can see the warning that I told you about. So previously, the file was called middleware, but now it's called proxy. Perfect. So immediately what you're going to notice now is that, let me just go ahead and I have a bunch of apps open. I'm just going to close some of them. If you attempt to go to localhost 3000, you are immediately redirected to a login page. So I'm just going to go ahead and sign in with Google here. And once I sign in, you're going to see that we're going to be redirected most likely to a 404 page but take a look at the url the url is actually organization selection so it's working as intended if the user is not logged in we can actually see that here i think this makes the most sense so we just added the environment keys and we didn't create these pages yet, but we're going to add them. But here is the flow, the proxy. We have a request, and we check if the route is public. If it's not public, we check if the user is logged in. If the user is logged in, we go ahead and check if the user has an organization. And if it has an organization, we allow it. So that's the happy path, right? So let's go ahead and do the other thing. If it's not public and the user is not logged in, we go ahead and redirect to a sign-in page. Then if the user is logged in, we check if the user has an organization selected. And if that is no, we redirect a logged in user to organization selection. So that is what's happening right now. So let's go ahead and create these pages, sign in, sign up and org selection. Now you've probably noticed that we actually already have sign in and sign up pages, but you also probably saw that those are on a different URL. So I'm going to show you how you can host your own sign in and sign up pages. Let's go ahead inside of app folder here. And I will create a new folder called sign in. And inside of here, I'm going to go ahead and add a catch all route, sign in like this, and then page.tsx. You can see that I'm using the same file name as this page. So page is a reserved file name in Next.js which basically enables you to do client-side routing. Sign-in is basically the name of the route So this will be an equivalent to sign And if you wondering what this is this is just a catch all route So whatever params whatever dynamic parts we simply want to make sure that this page is rendered on every single variation of sign in route. That's what that means. In here, we're going to go ahead and import sign in from clerk next js and then we're going to do a default export called sign in page we're going to add a simple div with flex minimum height of screen items center justify center and bg of background and we're going to render clerk's sign in page the clerk's sign in page will also have an appearance prop. Inside of here, we're going to target elements and give the root box a class name of mxauto and card shadow large. So let's go ahead and save that file. Let's copy this. Let's paste it. Let's rename it to sign up. If you have this prompt, which basically asks you if you want to update imports for sign up. Well, I just declined it. It doesn't matter, basically. It's cache. And make sure you also change the inner one to be sign up. You can select yes. You can select no. It doesn't matter. You can see it will just open this weird .next file. It really doesn't matter. You can save this, close this, and just make sure you close dot next. This is just cache. It will regenerate every time. Don't worry about it. Let's go back and focus on the sign up one. So the sign up will be identical. The only difference is we're going to change these three instances of sign in to be sign up. As simple as that. Great. So those were an easy sign up and sign in routes. And now let's go ahead and create org selection. Org selection will be a little bit simpler. It's just going to have a page.tsx. The reason these ones are having this, which I completely agree can be confusing, especially if you're a beginner and never seen this before. So that's basically just me following the documentation. So let me go ahead and click on something. So I open clerk documentation here. So Next.js, SDK reference, and let me go ahead and zoom out just a bit so I can find guides on building your own pages, authentication flows, and here we have custom sign-in or up page. So here it is. From their documentation, you can see that you need to render it on a Next.js optional catch-all route. So that's what this is. That's where I got that information from. And why does org selection not have that? Well, because org selection is not a part of official clerk documentation flow. It's simply a way that we are going to do it. So that's why this one is simpler. So let's now focus back on the org selection here. Let's go ahead and import organization list from clerk and next JS. Let's go ahead and export default org selection page. And let's go ahead and return some JSX inside. We're going to start by adding a very simple container, exactly the same as our sign in and sign up pages, flex, full height, items, center, justify center and VG background. And inside of here, we're going to render organization list, we're going to hide personal, we're going actually you can do just this you don't have to explicitly write true this basically means the same thing i'm going to add after create organization url after select organization url and i'm going to modify the appearance so elements root box and card great and once you save this file you can actually go back to your local host here uh and let me just go ahead and there we go you will now, after you refresh, see the following. Here's the thing. Organizations are not enabled by default in Clark. And thanks to their amazing SDK, you can actually enable it through your app. But just in case you are not seeing this message, here's how you do it through their application. So you simply click on organizations here and you hit enable organizations and select membership required. And let's go ahead and click enable. And then you can refresh this once again. And in here, you can see we need to set up our organization. So this is for our existing logged in user. So I'm going to go ahead and click join organization. And there we go. This is the page we just developed choose an organization and finally now we can visit our localhost 3000 with a button which says click me so that's everything we had to do super simple and we now have extremely secure authentication we have multi-tenancy built in and we have brilliant developer experience. Amazing. So, and I explained the flow. So the user can only use this application if they log in and if they select an organization. So if any of those are missing, we are not going to allow them to use this application. And yes, here is the note I wrote. I forgot to explain it to you. So the sign-in is an optional catch-all route for clerk multi-step flows. That's why it is required. And if you're wondering why did we need the hide personal on organization list, it's because we want to make this app team-based by default. Even if the user is a solo user, we're simply going to force a solo organization then. This makes it way easier to work with. And this is a industry standard practice when it comes to multi-tenancy. Perfect. Now what I want to do is I want to modify the page.tsx so you can see exactly how to log out and some other clerk components. So let's head inside of app folder page.tsx right here. And let's import organization switcher and user button from clerk next js. In here, let's export default function home. And what I'm going to do is I'm just going to add a simple div with flex, full height, flex call, item center, justify center, some gap, and the background here. Then I'm going to add a simple title, which will say welcome to resonance. And let me go ahead and refresh this so we can actually see that. So welcome to resonance. And then let's go ahead and add a div, flex, item center, and gap four. And inside, the organization switcher and the user button and just like that you can see this user's organization and these users account and this is really cool you can see that by using clerk you also immediately have account info settings you can even add more providers from here you can verify more email addresses. So a full out system, this is more than just a simple out library. This is an entire user management system. So on your dashboard side in here, you can ban users, impersonate users. So many things you can do from here. You can track their activity. You can help them debug things. You can add social accounts for them, give them passwords. You can track their devices. You can assign metadata. Basically, every single thing you could possibly imagine, Clerk allows you to do that in an extremely secure, compliant, up-to-date way. Basically, by using Clerk, it's almost like you hired a professional security team for you. and you can see we even have multi-tenancy enabled here by default which means we can immediately start inviting new users right so john doe test.com you can go ahead and give them a role of member or an admin send invitation and they will get an invitation on their email but also if they just log in into the app they will see an invite happening here and they will be able to accept it from here. So, so many things was actually added with just introducing clerk to our project. Brilliant. So, I believe that is everything we needed to do when it comes to clerk authentication. So, now it's time for the last part, which is our Prisma database. So, let's go ahead and implement that. So let's start by adding the runtime dependencies. I'm going to go ahead inside of my terminal here. And I'm going to install the following packages. Prisma adapter Postgres, Prisma client, T3 OSS environment next JS, and the PG Postgres. So out of all of these, the only one not exactly specific to Prisma and database is this one. I will explain what this is. It's going to help us a lot, especially in a tutorial environment where people often forget to use their environment variables. So since we're slowly going to be working more and more with environment variables, this is a package that helps a lot by breaking your app if you forget to add an environment variable so that's why we're going to add this package and yes you're reading this right if you're familiar with theo t3 that is their open source package and it's really good so again to repeat prisma adapter postgres prisma client t3 oss environment next js and postgres itself let's install that\nAnd then another thing we're going to have to do is to install dev dependencies. So that was step one. Step two is to install dev Prisma types Postgres dot environment and TSX. So we're not going to need these at runtime. We're going to need these for development purposes. after you've run these commands what I like to do is verify them inside of my package.json here so naturally you have a lot of new files here simply because of all the chatsy and cascading but take a look at prisma adapter postgres take a look at prisma client t3 oss and down here take a look at Prisma and TSX. I believe those along with some types are the ones we have added and Postgres of course. So it's not exactly important that you have the same versions as me here. I just want to make you aware what my versions are here. The ones that are actually important are Prisma ones but only the major version. So Prisma recently went up a major version from six to seven. So in case you had some older cache, which is still keeping you at version six, perhaps it would be a good idea to repeat these and just add at latest. So you have the newest version or you can specify the exact version that I am using at the time of this tutorial. So yeah, if you're on a higher version, I don't think it's that big of a problem. Again, except if you have a different major version, again, that is something that's out of my control. But if it's just some minor changes here, I don't think you will have any problem whatsoever. Great. So that is the base foundation of packages. So what we have to do now is we have to add a database. Now, with Prisma, you actually have two ways of doing that, and both are insanely fast. So using the link on the screen, you can visit Prisma Postgres website here, and you can see that you have two ways of doing this. You can simply run npxprisma init-dash database, or you can manually create a database from here. whatever one you prefer i would suggest that you log in first because by logging in you will basically be able to look at your databases here as well as on your local instance so once you're logged in you can see i have some projects here which i was testing. So again, you can create a new project from here as well. So let's go ahead and try by copying this command simply to see it in action. npxprisma init dash dash database. That is the one we are trying to do. And if you are if you're running this for the first time, you will most likely have to verify your account. That's why I told you to log in first. So you can easily just verify your account. In here, you can select your region. You select where your users are, not where you are. In my case, I'm the only user, so I'm going to choose Europe, but you usually select where your users will be. I'm going to give this a name of resonance. That is the name of my database, and now it's creating this project. So in here, it has now added even more files you will notice the prisma folder and schema dot prisma with a very simple generator client data source database right here and besides that let's also see what else was added prisma dot config dot ts was added as well and in here you can see that this file was generated by Prisma and assumes you have installed the following npm install save dev Prisma dot environment, which was our first step before we install Prisma. So we don't have to worry about that. If you want to confirm, it's always a good idea. Let's search for dot environment, you can see we have it in dev dependencies. Let's search for Prisma, we have it in dev dependencies as well. So we are good to go. And in here, there's something interesting. And that is the data source URL. You can see it's targeting process.environment database URL which means that this npxprisma init has probably created a database for us and also modified our environment file with database URL environment variable. So let's revisit .environment and that's exactly what's happening. This was inserted by Prisma in it. You can see nothing was reverted from our previous changes. And basically, we now have a database URL. It goes without saying you should not be sharing your database URL with anyone. I'm doing this for tutorial purposes and we'll remove this later. Excellent. So now that we have done that, let's just go ahead and I'm looking at my generated folder. It doesn't seem to appear anywhere yet. So that's perfectly fine. Let's go ahead and do what they tell us to do. And that is to create the first migration. So I'm going to go ahead and run npxprisma migrate dev with name in it. So that should just do a light migration of the current state of the Prisma schema. let's give it a second to do there we go already in sync no schema change or pending migrations have been found uh all right uh perhaps we should first modify our schema i believe that is the case okay so we confirmed that we have prisma.config you should have that too and you should also have Prisma.schema. So let's go ahead and add the models that we need here. So we're going to start by adding voice variant, enum. So every voice in our project can either be a system voice or a custom voice. That's the first thing. Then we're going to add an enum for voice category. What category does a voice belong to and this can be basically it can be your own list of things but I'm going to add the following list simply because that is from the data set of voices that I will share with you so because of that I'm I recommend that you add the same ones too so So audiobook, conversational, customer service, general, narrative, characters, meditation, motivational, podcast, advertising, voiceover, and corporate. Perfect. Now let's go ahead and let's set up a voice model. So what we did before were prerequisites in order to build the voice model. Now let's actually do it. Let's go ahead and make sure that this model has an ID of CUID. Let's go ahead and give organization ID an optional property. So it's a string, but it's optional. What this means is that if user creates a voice, we're going to attach an organization ID to it. But the reason it's optional, it's not because we're going to allow users to create voices outside of the organization. No. If organization ID is not present, it simply means it's a system voice. It's built in, right? Because we need to offer our users some built-in voices along with their custom cloned voices. And now let's go ahead and just add some generic properties. For example, the name of the voice, the description, which can be optional and now let's reuse the enum from above voice category which we just built and by default we're going to set each voice to be general then let's go ahead and add a language which is a required string and by default we're going to use a locale uh shorthand so and english us then let's go ahead and again use an enum from above which is the variant is this a custom voice or is it a system voice? Then we're going to add an R2 object key. This will be a reference to our storage where we're going to store the audio example of this voice. This can be any S3 compatible volume because we're going to be using AWS S3 packages in this project, but you can deduct by the name of this field. I will be teaching you how to use Cloudflare are two in this project for upload. Don't worry, we're not going to do that in this chapter. We're just preparing for it later on. And let's go ahead and add created at and updated at, which are date time and they're using these decorators default now and updated at. And let's also add indexes on the variant and organization ID to make queries for these more performant. one thing I forgot to mention is the syntax of this file if you're not seeing these bright colors as I am go ahead and search for Prisma inside of the extensions and simply install the Prisma extension all right so now that we have this we have to do the same for the generation model so let's start with the id which is identical to our voice one the only difference here for the organization ID is that it's absolutely required. So only organizations can create generations. Generations are basically text-to-speech prompts. If I type something like, hello, my name is Antonio, and I choose a voice and click generate that what this is That a generation a finished prompt just wanna you know well I think it better that I finish writing and then I'm gonna go over key design decisions here. So let's now create a relation with the voice. So each generation needs to have a voice assigned to it. So this will actually be optional. So voice ID string is optional. And the reason it's optional is because of the following line. So what we're doing here is we are establishing a relation with the generation and the voice. And again, it needs to be optional here. Using the relation decorator, we specify exactly what field we're doing the relation to. So this voice ID will be connecting to ID of the voice. So that's the references. And here's the important part, the onDelete. The onDelete is usually set to cascade. What would that mean? So yes, you can also use cascade here. What does that mean? That would mean if a voice is deleted from a database, every single generation done with that voice gets deleted as well. In most cases, that's not what you want. That's why we are using set null. So if someone deletes a voice from a database, we're not going to delete all of the generations. And that's why we need to make voice optional. So what are we going to do if a voice gets deleted? Well, we will have something called voice name like this. That is always required. So even if the original voice relation gets deleted, we are still going to have something to display to our users, at least in a way like this is the voice that had been used at the time. The voice is now deleted. and we're also going to have the actual text. This will represent the prompt. What is this generation saying? Like, hello, my name is Antonio. That's what the text is going to be, what the user will type into the prompt. And in here, we're going to need an optional R2 object key. Why do we need it to be optional? Not because it doesn't need to exist, but because we will create a generation before the upload finishes. So if an R2 object key doesn't exist, we are very simply not going to display this to anyone. It will be considered unfinished. If R2 object key exists, it means it is uploaded and ready to listen to. and now we're simply going to add some parameters depending on the generation so those are temperature which is a float top p which is a float top k which is an integer and repetition penalty which is a float you don't have to think too much about these values they are mostly relevant to the AI model, which we're going to use to generate text-to-speech, which is open source Chatterbox, which we're going to self-host later. So now let's go ahead and let's add created at and updated at and let's add the indexes. And in order to resolve this error, we have to establish a relation in the voice model itself. so before the timestamps let's go ahead and add generations so a voice can have many generations but a generation can only have one voice all right that's the type of relation this is so make sure to put an array here and save the file and just like that you should have no more errors here. All right. So now let's go ahead and run npx Prisma migrate dev. Let me see if I can reuse the name in it. I think I should be able to see because nothing happened last time. So make sure you save that file. And there we go. Applying migration. Perfect. And now let's also do npx Prisma generate. This will generate the Prisma client inside of source generated Prisma. So you will see two things. You now have migrations folder here and you also have generated folder here. So migrations are used well for database migrations. They are something needed. it's a whole another thing to explain so i'm just going to stop here i basically want to explain the difference why we are committing migrations you can see that these are not grayed out these are committed these are part of our change files but generated folder is grayed out and if you look at git ignore you will see that we have source generated prisma here that is because this is only used for type safety throughout our project. So it's not exactly, if I can say so, not used at runtime, right? So without this folder, we cannot develop. But technically, the app still works without it, if that makes sense. You can't, it's kind of like guessing. If you don't have this generated folder, you could technically have all of your queries working, but you would be guessing if they are correct or not. That's what the generated folder is for. It's basically like a bunch of types which read from our schema. So once we've added these two models, a bunch of type safety functions have just been generated in that folder and the relations and all of the fields. It's basically almost magical what was just done in that folder. All right. So now that we have that, let's go ahead and do the following. Let's go ahead and create our database instance. So we can actually query some things. So I'm going to go inside of lib folder and create a new file database.ds. So right next to our utils. And in here, I'm going to go ahead and import Prisma client from generated Prisma client. so this generated folder is exactly what we were just looking at so inside of source generated prisma and in here we have client so that's what this is you can see it's a very big type definition and let's also import prisma postgres from add prisma adapter Postgres. And then let's go ahead and define the adapter new Prisma Postgres connection string, and let's do process.environment database URL. And now what we have to do is the following. We have to create a singleton. Basically, let's add a constant global for Prisma. It's going to use the existing global variable. It's going to typecast it as unknown and then as an object which holds Prisma client inside. And then what we're going to do is we're going to define a new constant called Prisma, which will use global for Prisma dot Prisma. It will attempt to access it. If it doesn't exist, it's going to create a new instance of Prisma client with the adapter. and then what we're going to do is we're going to check if we are not in production we're going to store this new prisma instance inside of global for prisma dot prisma and i know i know why are we doing this this is very complicated for such a simple database instance why couldn't we just do this. Why can't we just initiate? Let me try and show you. Why couldn't we just do this, right? This is what will happen in production. This is exactly how production will look like. But because we are using Next.js or more specifically because we have hot reload enabled, we need to create this trick here. Yes, let's make sure we have this. Otherwise, it wouldn't work. The reason we are doing this trick, here is an explanation. We are basically doing this to prevent connection pool exhaustion during Next.js hot reload. Basically, when Next.js hot reloads, which is every single time you save a file, a new Prisma client instance would get created. And this would very quickly reach its limits with the pool, and you will get a warning in your terminal here that something is wrong, that you are initializing Prisma too many times. Because of that, we are storing Prisma inside of global because global, which is a shorthand for window.global, that's why it's available, is unaffected by hot reload. So it's a safe place to store it. And this isn't a hack that I made up. This is the official documentation. You can definitely find it in the docs. okay uh so here's what i want to do now here's the thing uh this adapter can now work um okay i'm thinking maybe it's not the best time to explain this right now uh one thing that i want to do right now i'm jumping everywhere right now sorry uh but we haven't really touched uh where is the t3 oss we haven't really touched this package that i told you to install so So I don't want you to think that I've forgotten about it. I will demonstrate on this example why we need it. But before we do it, let's actually try and do something. Let's go ahead in here and let's run npx Prisma Studio. And this will open Prisma Studio in here. And you can see we have an existing Prisma migration, but we don't have any generation or voice. And you should see the exact same thing in your Prisma data platform So here it is Actually I already logged in So Resonance 18 minutes ago You can see I also have a studio in here. So if for whatever reason your local studio is not working, you can visit the online studio here. But let's go ahead and go to localhost 5173. actually not 5173, it's 51211. Okay. There we go. So what I want to do now is I want you to click on voice. And then I want you to click on insert row. You can leave ID to be an empty string. And you can go ahead and find the name. And let's go ahead and give this a name, Antonio. And let's go ahead and give this a variant. of system and click save changes and click save and you should get the message rows inserted successfully so we just manually added something to our database so now we can go ahead and try and query it let me just refresh in this other page to confirm ice there we go so this is what I've just added, an Antonio voice. All right. So now that we have done that, let's go ahead and create a simple test page to learn how to use this new database lib. So inside of the app folder, I'm going to go ahead and create a new folder called test. And inside a new file page.tsx, I'm going to import Prisma from lib database, and I'm going to export a default asynchronous function test page. It's very important. This is an asynchronous function, and it's very important. This is called page. It's also very important you are doing this within the app folder. And in here, I'm simply going to do const voices await Prisma, and you can see how I have type safety on my models. So I can go ahead and do find many here. I can even go ahead and open where R2 object key and specify something, right? So you can see how every single property exists here. That is the power of that hidden generated folder. That's why we added it. So it analyzed this schema and it created an entire type safety environment for us to work with. So now in here we fetch all voices. So let's go ahead and just very simply return a super simple div padding 8. Let's go ahead and add a heading which will simply render the voices and then inside of parentheses, render the voices length. And then let's open an unordered list with some spacing and iterate over our voices using dot map and create a list item with key voice ID and list voice name next to voice variant and click save. So this is a server component which has access to the database. This is essentially the same thing as building an API endpoint. So this isn't exactly rendered on the client. You don't have to worry about this being exposed in any way. It is absolutely not. React server components are in itself a safe environment. Let's go ahead and visit this on localhost 3000. So localhost 3000 forward slash test. So localhost 3000 forward slash test. And in here, you should see Antonio dash system. If you see an error here, it is because of the database string. And it's just a security warning that the SSL mode prefer, require and verify are treated as aliases. So not something that's important for our use case. And in here, you can see Voices Antonio system. So if you go ahead inside of your Prisma Studio, and if you insert another row here, let me go ahead and change this to full, for example, click Save Changes, go to variant and select this one to be custom, save changes and click save. Okay, it needs to be system, save changes. Okay, it keeps failing. I am not sure why. Let me try refreshing this. insert row because we just managed to do the first one so I don't see a reason this one would fail. Let me try bar, maybe that's the better one. Okay, it's failing. Oh, oops, I understand why. Let's go inside a voice. It is because it cannot have the same ID. so let's just change the id to one two three and let's change this to foo and let's change this to custom and let's try again there we go now it works and if you refresh you now have two voices so antonio and foo so we are officially able to query our database amazing and now i can demonstrate what I wanted to do. So let's say, for example, inside of your lib database, you made a mistake, and you accidentally wrote database URI instead of URL. So what this would basically do, what it's supposed to do, it's supposed to break your app, but I think cache is saving us right now. Let me go ahead and try and do rmrf.next npm run dev. This is definitely supposed to fail. There we go. You can see now my app is failing. So if I fix this back to database URL and refresh, I think I have to do this again. rmrf.next npm run dev. rmrf.next is basically a super simple like cache purge. Now it's working again. So that's one example where this can happen. But the scary thing is there is no error within our IDE that we made a mistake here. But that's not the only place this can happen. For example, if I go inside of dot environment here, what if I change it here? Right? There are many places where this type of error can appear. And this is where our T3 package comes in handy. So let's go ahead and add it. Inside of lib, we're going to go ahead and create environment.ts. Let's go ahead and let's import z from Zod. You already have Zod installed. You can confirm that by searching for Zod. and it came installed with chatzn. So that's why you have that. And then let's go ahead and add create environment from T3 OSS environment next JS. And then let's go ahead and create environment here and let's add some server environment variables. I'm going to add database URL z.string and the minimum length of one experimental runtime environment will be empty and skip validation will be true by using double exclamation points by checking for process.environment.skip environment validation this will be important for production. So production doesn't break because production can't read from the local environment file. It reads environment file differently. All right. So now that we have this environment file, which has database URL defined as it needs to exist, we can go inside of our db.ts. And instead of using process.environment with not type save database URL, we can now go ahead and import environment and have type save database URL, reducing our chances of making an error. So if I attempt to do the same thing, you can see that now we have a big error in our editor. But that's not the only thing. Now, also, if I can demonstrate this, if I go ahead inside of my dot environment and if I do a mistake here, you can see I get an error, invalid environment variables. So because we expect database URL to be here, I need to make sure that my environment variable doesn't have a typo either. So we now have both validation within our code editor, but also within the actual dev server. You can see it's telling us that we are missing something, right? Invalid environment variables. Amazing. So now let's just go ahead and just quickly go over our database schema decisions here. So most of them, I think I've explained there. basically if a voice is deleted we're going to set null on the generation relation and we are going to rely on the voice name to tell the user what the voice was at the time. All right and then for the organization id it can be optional on voice because that's a system voice organization is required on generation because only organizations can do them and r2 object key is Cloudflare R2 path but as I explained this can be whatever you want, this can be any S3 compatible object. And that the infrastructure so T3 environments database prisma singleton and finally prisma Postgre adapter with a direct Postgres connection We are able to migrate we are able to run Prisma Studio and test page renders the voice list. So we have officially finished chapter one. Before we end the chapter, what we have to do is we have to create a GitHub repository so we can keep track of these changes. This will be very, very good for you to do. It's not required. If you want to, you can, you know, just pause here and go to the next chapter, of course. But I would highly recommend following me and see how I'm going to add this to GitHub. Because I'm also going to show you how you can immediately deploy this so you can see the changes on a real production instance. Before we can push this to a GitHub repository, I suggest doing the following thing. Again, if you want to avoid GitHub repositories and all of this, you can. But I think it's always a good idea to have a fully buildable and no lint error app before each push. So I'm going to go ahead and do npm run lint to see if I'm getting any errors here. For example, you can see that some components from ShadCN have errors, which means that my npm run build will fail in the same fashion. Well, actually, it didn't. Looks like they've made some differences between what is passable or not. My apologies. But I still think we should fix this, especially if you plan on doing some agentic work here. It's going to be very annoying that you have failing lints. So let's just do this together. And these are actually very simple fixes. So source components UI sidebar. You can find it here, source components UI sidebar, as you can see has some errors. Using this, I can find the error right here. And here's what I like to do for these. I just like to use quick fix and disable react, disable whatever it is for the entire file. So you can see that now I just have that at the top of my file. And now I no longer have any errors inside. So let's just go ahead and do npm run lint. And I'm going to do this, you know, until everything works. And looks like that was the only thing. There seems to be one warning here. And let me do npm run build to see if that will pass or not. Looks like that is passing too. If you want to, what I sometimes suggest is just going through these files. If you're using VS Code, it's automatically going to notice an error right you can see how they are becoming orange that's because we have some warnings but warnings don't break things so it looks like chat cn 3.8.5 mostly has fully functional uh i mean all of them are fully functional i meant to say 99 of them are clean when it comes to linting it is because chat cn uses radix and some other dependencies which sometimes skew version wise and that's where these errors appear so since we added all of these components i think it's a good idea to just you know make sure that ide is not throwing any errors looks like sidebar really was the only one with that simple math.random error with react purity so there we go We are clean. Everything here works just fine. Beautiful. So let's go ahead and let's create a new repository here. And I'm going to go ahead and select my organization here. I'm going to call this resonance. I'm going to keep it private. I'm not going to add anything. I'm just going to go ahead and click create repository. Since this is an existing repository, I'm just going to go ahead and copy this. And I'm going to do git. So what we have to do before pasting these three is we have to actually stage these changes and commit them. Because right now, they're just changes. You can, of course, use this interface too. But I would suggest learning git CLI so you don't depend on an IDE. So let's use git add dot to add all of our changes. So 76 files. Then let's do git commit. I'm going to do 01. And this will be project setup. Authentication and database. There we go. So we just committed all of those things. Perfect. And now we can go ahead and copy these three lines, paste them here. and that will synchronize that entire thing. So you can go ahead and refresh and there we go. We just did the very first commit, zero one project setup, authentication and database. Now that we have the repository set up, we are ready to deploy this application. The cloud service of choice is going to be Railway. Railway owns and operates their own hardware instead of reselling AWS like most platforms. So you're not paying that middleman markup. They have a free tier to get you started and their hobby plan is just $5 a month. And for five bucks, you're getting way more what you'd get from a random $5 VPS. We're talking auto-scaling, one-click databases, background jobs, cron jobs, deploy previews, all built in. And unlike serverless platforms where you hit timeout limits and can't even run things like live chat or real-time features, Railway lets you deploy your entire app in one place. Thanks to not being serverless, it means no cold starts and no limitations you often hit with serverless platforms. You just pay for what you use by the second, meaning no horror stories of bills being $40,000 because you got viral. All right, so using the link on the screen, go ahead and create an account and let's go into our dashboard. In here, I'm going to click on new GitHub repository. And let's go ahead and find our newly created repository. Once you find your project, go ahead and select it right here. And what we're going to have to do now is the following. It's immediately starting to deploy this. So it's just added CI CD to GitHub. That's another cool thing. When you refresh here, you will soon see a website you can visit here. and you will see the status of your CI CD if it's failing or not. Here it is. You can see deployments are happening. But this first one could fail, maybe not, maybe it will, because we need to update the variables. So using the raw editor here, we can share our entire .environment file here. So let's find .environment right here. And let's copy the entire thing. and let's go ahead and paste it like so. And here's what's important. We need to add one more thing that's not in our dot environment. So if you remember in our environment lib, we have this skip environment validation. Let's go ahead and add it here at the end. So skip environment validation will be set to true. So make sure that you've added this. Otherwise, it's going to keep failing. and click update variables. And then in the corner here, you can see apply for changes, deploy. So now it's applying those four changes. You can see the previous build has failed. And now we're going to see why it failed. So let me see. Oh, I think we also forgot to do one thing. Yes, you can see the error. Can't resolve generated Prisma client. That's my fault. I forgot to tell you one more thing that we have to do. So go inside of your package.json. The problem is this generated folder right here is not committed, which means deployed app doesn't have access to it, which means it's going to keep failing. So we fix this by simply adding post install to Prisma generate. So what we have to do now is save this file, make sure that's the only change, post install. Let's do git add, git commit. I'm going to keep it at 01 and I will just do package JSON post install. And let's go ahead and do git push. And since we've connected the railway with our repository now, you can see that that has immediately triggered another redeploy. So that is what we have just achieved by connecting Railway with our project. And I just want to make one thing clear. If for whatever reason, you don't have access to a free tier or you simply wish to deploy this project somewhere else, you can, of course, do that. You don't even have to create the GitHub thing. You don't even have to follow through my cloud provider service here. I would highly recommend that you do, but it will not interfere with you being able to complete this project. Don't worry about that. You will, of course, be able to have this working locally. Great. So we have two failed ones. And now let's go ahead and see if this one will succeed. I believe npm run build was just successfully built, which means we are on the right track. So let's go ahead and wait a bit for it to complete. And there we go. deployment successful. So now what we have to do is we have to add a public URL. So let's go ahead and first see whether everything was successful here. Great. And now let's go inside of the settings here. You can also change the branch this is connected to, of course. and then in here you can go ahead and find networking go ahead and find public networking and click generate domain and in here you can see it\nyou need to add the port your app is listening to. And this is actually not 3000. So you can see inside of deployment here, that the app is actually running on 8080. So whatever they suggest here is actually the correct one. So let's click generate domain. And yes, 8080 is the correct one. So let's click generate domain. There we go. And let's go ahead and open our app right here. And there we go. Sign in to Resonance. I'm going to go ahead and sign in. And since I already have an organization, there we go. Welcome to Resonance is officially deployed. And now here on our GitHub, you can see that our last commit has successfully passed. So going forward, every single chapter we do in the future, the moment we merge it is automatically going to be deployed. So you can immediately test all of your changes in production. For example, we just tested that clerk works, but how about I go to forward slash test? Let me zoom in so you can see. So my railway production instance, let's go to forward slash test. And here we are. I can see the list of voices. So we successfully tested our app locally and we deployed it on railway. Amazing, amazing job. So this was heavy foundation, but we did a lot. A lot of these things was just, you know, running commands, doing some slight changes, enabling authentication, multi-tenancy. But we did most of the complicated things in this first chapter. So don't worry. What we're going to spend the next chapters on is mostly UI and design. So we kind of relax our brain a bit from all of these things. So yes, definitely the next chapters are not going to be as feature heavy. So don't worry about that. amazing amazing job and see you in the next chapter in this chapter we'll build the complete dashboard experience a collapsible sidebar with organization switcher user profile navigation links and then we're going to build the home page with a personalized greeting animated wavy background, a text input panel, and some quick action cards. We're going to build this in the following order. We will start with a layout with sidebar shell. We will then populate the sidebar with navigation links. And finally, build a homepage with input and actions. And this is how it's going to look like. In order to get started, make sure you have npm run dev running and that your app is running on localhost 3000. Make sure to refresh to confirm everything is working correctly. Let's go ahead and start by adding a logo to our project. So for the logo, you can choose whatever image you want, and you have to put it inside of public folder. If you want to, you can use the link on the screen to get access to my source code. And you can go ahead and find logo.svg file, which I'm going to add right here. If you want to find the logos to use for demo use cases, not for real instances, you can go ahead and use logo.ipsum or untitled.ui. Both of them have amazing resources for demo logos. Great. Once you have added your logo here, we are ready for the next step and that's going to be building the dashboard layout. So what I'm going to do is I'm going to go inside of source instead of app folder and I'm going to create a new folder dashboard. Make sure that you write this inside of parentheses. When something is written inside of parentheses in Next.js, it tells it to omit that from the URL, which means that once I create page.tsx inside, it will be the exact same thing as having page.tsx right here, which means I actually have to delete this one because they are essentially on the same URL level. So let's go ahead and delete this page.tsx. We no longer need it. And now let's go inside of this new page.tsx and let's go ahead and just add a simple default export dashboard page which renders a div and the heading element which simply says dashboard. As simple as that. Great. And if you refresh the page everything should be working as expected and you can see my url is localhost 3000 so that proves what i just said that when you add a folder in parentheses it's simply used as a route group meaning that this folder is used for grouping things together our folder architecture it's not meant to affect the url in any way whereas these other folders like test is directly a part of the URL. You can test that by going to forward slash test. And you can see that we now have this loaded. So let's now go back to localhost 3000 and just make sure you have dashboard here. You can also remove this test page since we will not be needing it. It was just for demonstration purposes. Once we have that, let's go ahead inside of this and create layout.tsx. And inside of here, we're going to go ahead and import cookies from next headers. We're going to import sidebar inset and sidebar provider from components UI sidebar. Let me go ahead and collapse so you can see more clearly. So components UI sidebar is a chat CNUI component which we've obtained in the first chapter. And then let's go ahead and do a default export of dashboard layout. The dashboard layout needs to be a default export. Make sure it's an asynchronous function because we are going to have to await cookies inside and you will see what we'll use cookies for in a second. And the only prop this function will accept are the children. So I simply map the types here to be children React React node. Great. Once we are in here, we can very quickly see some results. So I will just type layout and save this file and immediately you will see a different on our localhost 3000. So you might be wondering why did this layout file override the page file? What's going on with that? Well, both page and layout are reserved file names in Next.js. Page is used to represent a route, whereas layout is used to represent a route or route groups layout. So why is it overriding this content? Well, because we are using it incorrectly. We're not utilizing the children. So if you want to see what I'm talking about, let's go ahead and wrap this inside of a navbar and let's render the children below. And now you will see both layout and dashboard written here. so to show you what this layout file does go ahead and create a new folder here for example users and a new page inside once again so your dashboard folder should have a page and users and instead of users another page so i'm going to go ahead and just export default function users Let me fix the typo here. I can't seem to fix the typo. And in here, I'm just going to say h1 users. And let me go ahead and just add text to Excel font bold. So if I go ahead and go to localhost 3000 forward slash users, let me show you. So again, we don't write dashboard here, right? There's no need to write dashboard in the URL because it's in parentheses, meaning it's omitted. And you can see what happens. I still have the layout text above end users. So that's what the layout file is used for. You can put it inside of a folder or a route group folder like we are right now. And this layout will be preserved regardless of the route we are on, as long as that route is defined within a folder. So that's what the layout is for. And it's a perfect solution to host or to render the sidebar. This way, regardless if we are on dashboard, voices, text-to-speech, or voice cloning, that will render in a page.tsx document here. But the layout will be unrendered. I mean, it won't re-render. It will stay rendered once and that's it. So that's the perfect place to put the sidebar in a layout file. And this is a page file. I hope I cleared that up. Let's remove the user's file for now. We don't need it. And let's go back to our layout. So you can now remove the navbar layout from here. And you can go back to localhost 3000. All right. So what we're going to do now is we're going to build our sidebar using the sidebar composition from ShatCN UI. So I'm going to go ahead and render sidebar provider as the top argument. Default open for now can be set to true. And class name here will be set to HSVH. And let me fix the class name typo. All right. and now let's go ahead and add sidebar inset which is another compositional component to make the sidebar content look good let's go ahead and give sidebar inset minimum height of zero and minimum width of zero which serves like some form of css reset so we can have proper heights and overflows and scrolls within our pages, which are rendered, well, here inside of Sidebar Inset. Okay, and to make this semantically correct, let's render children inside of a main HTML element. Let's give the main HTML element. You can see how the moment you forget to add some class names it definitely affects the entire page here So let for example this is now broken We expect this dashboard to be fully centered right You can see using flex, item, center, justify, center, that's what we expect. That's why it's important to write proper composition. So we need to add another reset here, flex1 and flex column. And once you save that, you can see that it's back to normal. Great. Now, let's go ahead and utilize our cookies. So let's define the cookie store by using await cookies. And let's go ahead and extract default open to be cookie store dot get sidebar state question mark dot value equals true. This is all in one line. Let me expand so you can see like this. So basically, this is a way to preserve the state of the sidebar because sidebar can be closed or opened. So if the user keeps closing their sidebar and then refreshes, it gets quite annoying that they have to close it again. So this is a way to keep it in a state that the user last decided it should be in. So now let's use this variable here. And if you're wondering where do we get this sidebar state, I'm pretty certain that if you command click inside here, you will be able to find this sidebar cookie name sidebar state right here. So just confirm that it's the same. You can even use a constant here. But the last time I exported this and attempted to use a constant, it actually caused some hot reload problems. so that's why I'm writing a magic string here and we need to do optional operator here because the cookie doesn't have to exist all right so we now have the provider we have the inset but we actually have no sidebar yet so that's what we're going to do next and I'm just going to prepare the import for that so I'm going to go ahead and add that right here so dashboard sidebar will come from, I'm not going to do it yet, because you have no idea where I'm importing this from. So let's actually build it first so you can see exactly where we're going to import this from. Sorry for that. So inside of source, let's go ahead and create a new folder called features. And this is why I told you source folder is important because of the way we're going to architecture our app. So I want the features to be inside of the source. It just wouldn't feel the same if features folder was on the same level as prisma and public so that's why i want it in the source folder so we're going to be using a features split architecture here so i'm going to create an entity or a feature called dashboard and then in here i'm going to create components and then in here dashboard dash sidebar dot tsx let's go ahead and mark this as use client and let's immediately import a hook from next navigation because we couldn't be able to do this if we didn't mark it as use client. Because if a component is not use client, it's a React server component. And those are essentially API routes just in a JSX way. It's not exactly the proper definition. I'm just using that definition in case you've never heard of them before. So it's easier for you to understand. All right, so image from next image, use path name and lucid icon from lucid react lucid react is a package that we get because we are using chat cnui what we have to do now is we have to import a bunch of packages from components ui sidebar so that's going to be sidebar content footer group content and label and then after that the rest of them which is going to be header menu menu button menu item rail and trigger so you can pause the screen and just import all of those from components UI sidebar. Then let's go ahead and import skeleton from components UI skeleton. Let's go ahead and switch it up by importing organization switcher user button and use clerk from clerk next JS. So we're going to use this UI components and a hook to render the organization switcher and the user button. We already did this in the first chapter when we rendered it on a blank page but now we're going to go ahead and use it in a proper layout. Now let's import all the icons which we are going to need from Lucid React, Home, Layout, Grid, Audio Lines, Volume 2, Settings, and Headphones. And last but not least, let's import Link from NextLink. Let's go ahead and add some interfaces. Menu item with a required title string, optional URL string, required icon, which is a type of Lucid icon which we import from somewhere. Okay, so we import it here, which means we can do it here again. So let's import lucid icon. And I think we can prefix it with type lucid icon. There we go. So we reduce the amount of imports above. And an optional on click. Then let's go ahead and create an interface nav section props, an optional label, items, which are an array of menu items defined above, and a path name, which is a string. Then let's go ahead and create a function nav section. So let's go ahead and create this function with props label items and path name. And in here, we're going to go ahead and return a sidebar group component. The sidebar group component will check if the label exists because it can be undefined. And if it exists, it's going to render sidebar group label component with label content inside and a small class name to specify the text size, make it uppercase, and give it text muted foreground. All right. Then let's go ahead and open up sidebar group content component, and then let's close it here. And let's add sidebar menu inside. And what we're going to do here is we're going to iterate over items.map. I mean, we're So items are simply an array of things we want in our menu. So what we're building now is basically this. So this is like one, what is the nav section, right? We're going to have one nav section, second nav section, third nav section. It's basically like a collection of menu items. That's what we're building a component for now. And when we iterate over these items, we're going to render them instead of a sidebar menu item component. so make sure you give each of these a key of item.title because we assume these are going to be uniques and let's go ahead and render sidebar menu item here and now first of all let's go ahead and mark it as child if we have item.url so by adding double exclamation points you can turn any type into a boolean here so if it exists it's going to be true If it doesn't, it's going to be false. As child is needed because if we have item.url, we're going to have to render a link element. And the proper way to use a link element instead of next.js and the button is to use as child property. So this button then becomes the link element. And let's go ahead and check if something should be active. So when should we display an item as active? For example, right now, this scenario, I am on a homepage, on a dashboard page, and you can see that this dashboard page is highlighted. So that's what isActive is. So if I click on Explore Voices, it should turn green or in our case, just active, right? That's what isActive prop is doing. So let's go ahead and create some logic for that. When should a menu item be active? Well, first, let's check if we have item.url. If we do, let's go ahead and check if item.url is a root page. And if it is, let's use a double ternary here to check if the current path name is also forward slash. So the path name is something that we have right here. All right. So if that is true, then let's go ahead and check if path name starts with item.url. my apologies that's not what i meant to say uh if path name is not uh a forward slash then let's check if it starts with item.url otherwise false the best way to understand what this does is to see it in action uh looking at it it's quite confusing to understand what it does uh i'm sure you you are thinking why can't we just do this right uh you can but very soon you will notice some edge cases. So this is kind of like an advanced way of doing that in a bit more complicated way, but the results are actually quite good. Let's go ahead and pass in item on click. If it exists, this will be used, for example, in voice cloning. So voice cloning will not be a separate page. It's just going to be a dialogue. So that's why we need an on click option. And that's why we have optional items.url for example. All right so that's on click here. Tool tip is going to be item.title and then in here let's finally render something inside. So item.url check. If we have it let's use link element. Let's render item.icon and span to render item.title. each link element needs to have an href so let's go ahead and give it item.url and if that's not the case so if we don't have item.url in that case let's render a fragment and the exact same content inside all right now i just want to see what i'm doing wrong here because i'm having some error here so let me just check so this is properly closed return does it have a proper one it does let me see what exactly is missing here I think another parenthesis might be missing So we iterate over items Let's see where sidebar menu item ends. It ends right here. I think I'm just missing another parenthesis. I am. All right. So that definitely helped. And I want to stop here because we wrote so much code and didn't really see what it's doing. So there is just one more class name we have to add, but I don't want to bother you with that until we actually render something. So let's actually export function dashboard sidebar below it. In here, we're going to initialize the path name and use clerk. Then let's go ahead and define main menu items and give it a type of menu item in an array. so the first one will be the dashboard with title dashboard url forward slash and an icon of home and the second one will be explore voices with url voices and layout grid the third one is going to be text to speech with text to speech url and audio lines and the last one will be voice cloning with no option at all right so i mean no url at all so just icon great then let's go ahead and create other menu items so this was main menu items now below it let's create others menu items exact same type open an array and the first item will be settings with an icon of settings and an on click, which simply uses the clerk hook to open organization profile. And then the second element will be help and support. This can lead to your documentation page, or it can lead to an email. So you can open a user's email client by creating a href to mail to. This is normal HTML. This isn't Next.js specific. And in here you can write whatever email you want. So this is, for example, what I use for my customer support. And give it a headphones icon. So that's the other menu items. We now have two navbar sections that we can render. So let's go ahead and start rendering things. I'm going to go ahead and return a sidebar component. And I will give it a collapsible of icon. You will see what this is for in a moment. Let's go ahead and render the sidebar header. Now the sidebar header should have the following class names. Flex, flex call, gap four and padding top four. And now we're going to go ahead and open a div element. Now this div element will have a class name of flex, items center, gap two, padding left of one. And now these are important. so uh this is all one class name group data dash collapsible icon justify centered so let me zoom out so you can i think i need to zoom out even more or maybe move this down here so you can see so group data collapsible icon justify center is one class name and group data collapsible icon pl0 is one class name as well right so make sure that you don't add any spaces when writing this you can see it's all one class name all right so now that we have that in here let's go ahead and render an image and this image will render logo svg this should match exactly what you have added in the beginning of this chapter to your public folder. So I added logo.svg. So you should target whatever you have added in here. Let's give this an alt of resonance. That's the name of our app. Width is 24. Height is 24. Class name rounded small. And then let's go ahead and render a simple span element with a text resonance. Now we should give this a class name as well. So I want to add the most complicated one first, which is another group data collapsible equals icon hidden. So it's basically the exact same one we've been adding three times now. and then let's go ahead and add the rest so font semi-bold text large tracking fighter and text foreground and let's go ahead and add something next to this span element which is a sidebar trigger which can be used to collapse or expand it but it's only going to be visible on mobile meaning it's hidden on large devices and give it ml auto so it's pushed all the way to the right side all right now i think we can already preview this even though we don't use any of the menu items we have dashboard sidebar now so let's go back to this layout file which we started to work with in the beginning and we can now import dashboard from features dashboard components dashboard sidebar and then let's simply render dashboard sidebar right here I'm going to go ahead and expand and zoom out and make sure you zoom out too so you need to be on desktop mode to see this and now you can see that we have our sidebar right here so I'm going to be zoomed out a lot simply so I can actually see the sidebar here all right now let's go back inside of the dashboard sidebar and let's use the path name and our menu items thankfully we already built the nav section component so we actually have uh quite easy work here so outside of this div uh let me just see so okay outside of sidebar header let's add a div with a class name border bottom border dashed and border border and then let's add sidebar content here and inside let's add nav section component which is a self-closing component pass it items to be main menu items and path name to be path name like this go ahead and save this and now you should see dashboard explore voices text to speech voice cloning so all of those options are now here now let's go ahead and add the others so i've purposely omitted label for this nav section because i don't think it needs to have it but for the other ones i want to give it a label for example and let's pass in our others menu items and path name again and now you should have two sections the first one and then others with settings which should open organization workspace like this and help and support should open the email client all right now that we have this let's go ahead and go back inside of the sidebar header and let's open sidebar menu once again sidebar menu item so we follow the proper composition and render organization switcher from clerk inside now in here you should see john's organization here are a couple of props i want to add i want to go ahead and pass in hide personal so we never allow the user to switch to a non-organization mode and i want to go ahead and give it fallback fallback is basically what should render here before this is loaded let me go ahead and this seems to have collapsed somehow okay to give you an ability to uncollapse it i'm just going to pause this in case this has happened to you the to give this an ability to uncollapse go down here where sidebar is almost closed and render sidebar rail what this should enable you to do is have this border clickable. So the right border should be clickable now. All right. In case that isn't working for you, here's another thing you can do. You can go inside of app folder, dashboard, page, and in here, you can render a sidebar trigger from components UI sidebar. And you can see that now next to the dashboard text, you can actually trigger the sidebar. The reason this is working just by adding a component is because in the layout file, we added a sidebar provider, meaning that each page rendered within this route group has the proper context to communicate with the sidebar provider and sidebar context so that's why this is working so you can now even toggle it on mobile too that's a cool trick and you can close it from here so yeah perhaps it's easier to add it here so we can develop in the mobile mode as well great so make sure that you add sidebar rail still because that's what allows it to collapse on desktop mode. All right. So we've added organization switcher now, but the problem is when you refresh, you see it kind of flashes. It doesn't immediately appear. So in order to resolve that, we can use something called fallback. And fallback here can have a component called skeleton. So skeleton, which we import from ShatCN UI, Components UI skeleton. And let just go ahead and give it a class name to match the appearance of organization switcher So height of 8 full width group data collapsible icon size 8 This is all one class name Rounded medium border border border I think we don need this actually. And background color of white. So let's go ahead and see it now. There we go. You can see that now there is something like filling up the space. The problem is it still kind of jumps. Don't worry. That's because we haven't added proper class names to the organization switcher itself. And just to zoom out so you can see group data, this is all one class name. I want to make sure you're able to write it properly. So now let's go ahead and add appearance to the organization switcher so it looks like we expect it. If you don't want to type this out by hand, you can always pause the video, go inside of my source code, and simply go inside of features, dashboards, components, dashboards, sidebar, and you can just copy whatever I put inside of here. Simply because there isn't much to learn about styling this except knowing that you can style it. So we're first going to modify the root box. Now, the root box is going to have with full with exclamation point at the end, which makes it required. We're then going to use another group data collapsible icon and change the width to auto. And pretty much every single class name here will end with an exclamation point. So keep that in mind. Then another group data collapsible icon to enable flex. and then another one group data collapsible icon to enable justify center great now after root box we're going to go ahead and modify the organization switcher trigger let's go ahead and add the following class names here full with justify between background white border and border border. We're not done yet, so it's going to have a couple of class names. Let's go ahead and add a rounded medium PL1, PR2, and PY of 1. Then let's go ahead and add gap3. And let's add another group data class name, collapsible equals icon with auto. and another group data group data collapsible icon padding one so the group data thing is actually just making it responsive simply because on desktop you can see that we have this responsive mode and you can see it's not fully working for organization so that's why we are adding the group data class names to recognize when it's in the collapsed mode so we can properly display it so let's go ahead now and add another class name here for organization preview and just make it gap two then let's go ahead and add organization preview avatar box with size six and rounded small then let's go ahead and add organization preview text container this should have some text changes text extra small tracking tight font medium text foreground and then another group data collapsible icon class name which simply forces it to hide when in that mode then let's go ahead and let's add organization preview main identifier to have text 13 pixels and let's go ahead and do another one, organization switcher trigger icon to have the following class name, size four, text sidebar foreground, group data collapsible icon hidden. And now you can see it's fully compatible with being collapsed. Amazing. And it still works as expected. Beautiful. So now that we've done those changes let's go ahead and also do those changes to our nav item so our nav section here renders the sidebar menu button but we never actually give the sidebar menu button any class names so that's what we're going to do now we're going to make it the same height and same padding as the organization switcher but this will be easier because we don't have to target multiple elements just one so let's start with height 9 px3 py2 and text of 13 pixels let's change the tracking to tight let's go ahead and give it font medium border and border transparent the reason we are making it transparent is so that when it turns into active we can simply change the color of the border because if you don't do transparent you will see a visible layout shift every time it becomes active so this is one class name like this data active true border border all right uh and i think that should be enough let me just go ahead and see there we go perfect and if you want to add one more cosmetic change for this I wouldn't recommend writing this by hand if you want to you can but you can just copy from my source code again another data active true and just go ahead and add this specific shadow so you can see why I'm telling you that you don't have to write this by hand it's just a subtle effect which I like I'm not sure if you can even notice it if you can't you don't have to implement it that's why i said you don't have to write this part by hand you can just copy it from the source code or just don't use it this is just ui and cosmetic anyways all right and speaking about those shadows we can actually add the same thing to an organization switcher if you want to to make it look the same so in the organization switcher here. If you find organization switcher trigger, you can go ahead and paste another shadow here, which is essentially the same or very similar shadow as I've just defined above. Again, I don't expect you to do this, except if you really, really care about UI and cosmetics, you can copy it from the source code. And now it kind of has a similar shadow here. all right now that we have that there is one more element that we have to add and that is inside of sidebar footer so that is the user button so after sidebar content let's go ahead and simply add a dash to border here and let's open up sidebar footer sidebar footer should have a class name gap 3 py3 and inside a normal usual composition sidebar menu sidebar menu item let me go ahead and open it there we go and let's render the user button inside so this is just before we render the sidebar rail. And now at the bottom here, you can see it's interfering with this developer indicator. Here's a little trick you can do. You can go inside of, I mean, this indicator comes from Next.js and you can modify it instead of next.config. Dev indicators false like this. And then that should hide it. There we go. And you can now see the user button here at the bottom. The problem is it doesn't really have the same design as all others. And now if you want to leave it like this, you absolutely can. So again, we're just doing cosmetics now. But if you don't, you can go ahead and do the following. You can go ahead and enable, make it look a little bit better by adding show name, which kind of makes it look better. The problem is it doesn't take the full width and height. And it also, it flashes, right? So if I zoom out, you can see the layout shift happening at the bottom, moving the entire thing around. So I'm going to go ahead and add a fallback here. Skeleton, class name, height 8.5, full width, group data collapsible icon size 8. So this is a single class name. rounded medium border border border is not needed actually actually it is it is so make sure you add border border and background color of white so that's going to make it jump less and now we have to also change the appearance of the user button if you want to you can just go inside of the source code it's free and you can copy the appearance in the user button just find features dashboards components dashboard sidebar so we're going to go ahead and add the elements inside of here and let's go ahead and add class names for the root box the root box will have with full group data collapsible icon with auto and then we're going to have group data collapsible icon flex group data collapsible icon justify centered. So not enough to actually see any changes because we have to now add some class names to user button trigger. So in here, I'm just going to go ahead and add with full justify between then I'm going to add background white border border border arounded medium and then we're going to add some padding here so that's going to be pl1 pr2 and py1 there we go you're starting to see the border here and now i'm just going to go ahead and add a shadow again you can copy and paste this i don't expect you to write it yourself\nAll right, we have that. Now, let's go ahead and see what else we have to do here. Let's go ahead and add group data collapsible icon with auto. Let's add group data collapsible icon padding of one. and let's add group data collapsible icon after hidden then let's go ahead and add another element here user button flex row reverse and gap two then let's go ahead and change the user button outer identifier so this is another text size force and let's go ahead and add some tracking tracking tight font medium text foreground let's go ahead and add a pl of zero and group data collapsible icon of hidden and let's go ahead and change user button avatar box to be size six as i said you can just copy a bunch of these from my source code. Another one I forgot. In the user button trigger, you have to add another class name, which is this one. At this point, I think you've definitely noticed it's just easier to copy from the source code simply because it's a bunch of confusing class names. It's basically just cosmetics to make this look a little bit nicer. And you can see all of them kind of match now. And you can see there's minimum shift going on here. Great. So all of this seems to be working fine now. If you attempt to go to any of these, you will get 404. We have a nice working organization. We can open the organization settings from here, or we can click on settings here. We can go ahead and manage our account from here. And we can even sign out. and that should redirect us to this page. One thing that I'm noticing, it's not working as expected here, is when I log out, yes, when I log out, I'm not redirected to my login screens. So that's something I forgot to do and it's a very, very simple fix. So you have to add additional class names to your .environment for the next clerk pages. So it's these four. Next public clerk sign in URL, sign up URL, after sign in URL, and after sign up URL. This way you make clerk aware that you're using custom sign in pages, which should match exactly these routes. Okay. You don't have to do it, but since we developed custom ones, it makes sense that it redirects us here, right? And now after I log in, there we go. John's organization. Beautiful. So that is part one done. Now let's go ahead and build the last part, which is essentially the homepage with inputs and actions. So in order to build the actual dashboard page, we need to add Vavy background. So this is a cool effect from as eternity UI, if I pronounced it correctly. And it's basically this cool background effect. Of course, if this is not something that interests you, you can skip this. So we can add this using chat CN simply because chat CN is a registry. So I'm going to go ahead and run this command. In case you cannot run this for whatever reason, if it's changed, if it looks different, always remember you have access to the source code and you can just go ahead and find the component you need. So I'm going to go ahead and run npx chat cn and I'm just going to use the same version that I've been using which is 3.8.5 I believe. Let me go ahead and confirm it is 3.8.5 all right. Simply so you know it's compatible with all of the other components I've added. so let's add as eternity of av background and now you will see what this will do so it's going to install a dependency and it's going to create one file so let's go ahead and look at the packages which were added so the only package which was added is simplex noise so if for whatever reason you can't use this. You can do manual npm install, simplex noise like this. And the component which was added is inside of source, components, UI. And it's probably all the way to the bottom because of the letter alphabetical, a wavy background. You can see it only depends on the simplex noise package. It has a bunch of errors, so I would recommend that you immediately go inside of Vavie background here and just go ahead and do a quick fix and disable the following one. So disable this for the entire file. So Slinth disable TypeScript Slinth no explicit any. And then go down here and find another one, expected var, use let or const. You can just do quick fix and disable no var for the entire file simply because the component works as is it's just cosmetics we don't need to worry about this too much by disabling this lint rules it will not prevent your app from failing to build so that's why we are doing that to make sure no components are preventing the build from happening great so now that we have vavy background if you are interested in using wave background, you need to do one change here. So alongside wave opacity, let's also introduce wave y offset. Wave y offset is going to be a new custom prop we create, it's going to be optional, it's going to be a number. And let's save the file. And let's go ahead and find where we, let's go ahead and do context line two. All right. So search for context dot line two and go ahead and delete this comment. And we're just going to do a slight modification. so it's going to go ahead and use not 0.5 it's just going to be plus wave y offset and it's not going to include the height in the calculation so it's just going to be x y and plus wave y offset again you can just copy this from the source code okay so we just did this slight modification what I want to do now is I want to create a component called hero pattern. So I'm going to go inside of features dashboard components hero-pattern.tsx. I'm going to import that wavy background which we've just modified and added. I'm going to export function hero pattern. I'm not using a default export here because this is not a reserved file name like page or layout which require a default export. So I'm using a named export in all of my components if you haven't noticed. And in here we're just going to go ahead and do a very simple div with the following class names. Pointer events none. So this isn't clickable. Absolute. In set zero. Hidden. overflow hidden and only visible on desktop devices and now we can render the actual wavy background and let's just go ahead and give it the following props. I'm just adding some colors here you can of course modify this some background fill blur of three speed of slow then we're going to add some wave opacity and wave width and then we're going to use our new prop wave y offset. Let's go ahead and add a container class name of height full and let's give it a class name of hidden by default. All right so now that we have the hero pattern I want to go ahead and render it so you can see your work. So we're going to go ahead inside of features dashboard and we're going to create a new folder called views. And inside of here, let's go ahead and create dashboard dash view dot t s x. I'm going to import hero pattern from components hero pattern. And I'm going to export function dashboard view return div with a class name of relative and render hero pattern like this and the you can import this either like this or you can always explicitly import from dashboard components whatever you prefer all right and now that we have the dashboard view let's go ahead inside of app folder dashboard page.tsx and let's go ahead and change the return to be dashboard view. This way we develop the UI inside of the features folder and not inside of the app router. The app router is used for routing, prefetching and server components. You can remove the sidebar trigger import as well. And now when you expand on desktop mode looks like nothing is appearing. I think it's because we're missing some other components. So for now I think we did everything correctly. Let me maybe try commenting some things out here. Is it because of the class name hidden? Trying to make it appear. All right I still can see it Never mind we gonna see it later So leave it unchanged And now let go ahead and create a page header component which we're going to use to bring back the sidebar trigger on desktop mode. So we're going to go ahead inside of source components, create a new file page-header.tsx. So I'm purposely not developing this instead of the UI folder. I want to reserve the UI folder for chat-c and added files. So I know that all files inside are not something I've wrote, but all other ones are. So in here, we're going to import headphones and thumbs up from React, Lucid React, link from next link. We're going to import button from components UI button, sidebar trigger, and CN from lib utils. Let's go ahead and export function page header with title and class name for props. Class name is optional. And in here, let's go ahead and return. Now I want to start with a div, which will use the CN function. this cn function allows us to write dynamic class names in tailwind. So cn accepts an infinite amount of parameters. So you can see it really does not have an end to how many parameters you can write. The point is this works better than doing something like this, flex blah blah blah, and then class name. This is because if you pass undefined, it would literally render undefined in your list of class names. CN util takes care of that and many other things such as overriding class names, proper ternaries, and things like that. So it's always recommended that you use this. And it's honestly better developer experience too. You just pass your dynamic class here. All right. then let's go ahead and open a div with flex items center and gap two let's go ahead and render sidebar trigger and let's go ahead and render a heading with text large font semi bold and tracking tight and render the title inside and then what we're going to do is just render two buttons. So flex item center and gap three. So this will be rendered on the left side and this will be rendered on the right side because we are using justify between. So within this container, the first element is on the left side and the other element is all the way on the other side because we justify between those two elements. So let's go ahead and add a button variant outline size small and as child property. And in here, let's just go ahead and add Ahrefs to our support emails. Let's render thumbs up. And this will be if anyone wants to send some feedback, but hide this on mobile, it should only be visible on desktop. And then we're going to do the exact same thing another button another link to our support and just need help text size outline as child everything is the same great so now that we have the page header we can actually render the page header too so let's go ahead inside of our dashboard view features views dashboard view and let's go ahead and render page header. This is a reusable component that we're going to use in text-to-speech, voices, everywhere. That's why I didn't create it in the dashboard features, but rather in our common components. Page header should have a title of home and a class name of LG hidden. So you can see on the mobile, you should have home and you should have an ability to open this. But on desktop, it's going to be hidden because dashboard is a very specific page which will have its own header, all right? And these two buttons should just open an email and nothing more. If you want to, you can use dashboard instead of home so it matches what you see in the sidebar. All right, now that we have that, let's go ahead and go below the hero pattern and add a div class name relative space y8 padding of four lg padding of 16 and in here let's develop the dashboard header so dashboard header let's go ahead and develop that now let me just go ahead and find this component here okay so dashboard header is currently not defined so obviously it's throwing an error so let's go inside of features components dashboard header dot t s x let's mark it as use client import use user headphones thumbs up and link from next link. Let's also import button from components UI button. Let's go ahead and export function dashboard header and let's use use user hook from clerk. And from here we can extract is loaded end user. Now we're going to greet the user with their name thanks to this information from clerk. So let's do some simple composition here flex items start justify between then let's go ahead and open a div space y1 let's render a paragraph nice to see you with text small and text muted foreground and then in here let's go ahead and render a heading element a heading element will have a class name of text to excel on large text 3xl font semi bold and tracking tight. It will check if the clerk has loaded. If it has loaded, go ahead and open parentheses and check if user object has full name. If it doesn't, fall back to user first name. And if it doesn't, fall back to there, meaning we weren't able to load the user's name. and if it didn't load just use three dots like this and now let's go back instead of the dashboard view and let's import dashboard header again if you want you can use dot dot or you can directly import from here and you can see how while it's loading it's three dots and then it says nice to OCU John Doe or whatever your account name is. Brilliant. Now that we have that, let's go outside of this div, which is encapsulating the header. And we're just going to go ahead and repeat those buttons. So I think it makes sense for us to go back inside of page header. And let's just copy them. As I said, this is a very specific case for dashboard page. Usually we will not do this much code duplication. So go back inside of components, page header, and copy the div, which holds these buttons, the feedback and the need help button. So just copy those two. Go back inside of the dashboard header and just add those buttons in here. Let me just fix the indentation here because I feel like something wrong. all right and now in here let's see what we should do so the first thing we should do is hide these on mobile like this and change the flex to only appear on large devices like this. So now you can see that mobile is using the shared page header, but desktop has its custom buttons here. And you can see the wavy background starting to appear. So yes, I was right. We need to add more elements before we can see it. So let's just keep developing. I wanted to show you the wavy background earlier, but I ended up just, you know, confusing you with all the changes. Sorry about that, but we will see it very soon. All right, so now we have a nice look both on desktop and on mobile. It looks very, very nice. What we have to do now is we have to develop the text input panel. before we can do that let's go ahead and prepare another feature folder so instead of features we're going to have another feature called text to speech we are not developing it yet but we are kind of preparing for it you can see that this element here start typing or paste text is the text-to-speech functionality, but we are kind of borrowing from this now. So I'm going to go ahead and go inside of text-to-speech, and I will simply create a new folder called data, and inside constants.ts, and I will export text maximum length and make it 5,000. So I technically didn't have to do this already but since I know that belongs to that feature I'm going to develop it here. Great so now that we have this let's go ahead and let's develop the text input panel. So we're going to go inside of components and let's do text input panel dot t s x. Let's go ahead and mark this as use client. Let's import use state, use router from next navigation, coins from Lucid React, badge from components UI badge, button and text area. And then we can also go ahead and import our new text max length from features text to speech data constants You can also let it autocomplete if you ever let me see if it will work Text max length Yes you can see autocomplete works well So now let's go ahead and export function text input panel. And for now, this won't be a real form. So usually when we develop forms, we're going to use 10 stack form. But since this isn't an actual submit form, it's just decoration. This is just the homepage, which will redirect the user to text to speech functionality once they type in something here. So this is mostly just marketing page right here. That's why we will use a simple use state. And that's why we're also going to need useRouter to redirect the user later. So let's go ahead and prepare a very simple handleGenerate function. So when user clicks generate in this text input, what's going to do is it's going to take the text the user has written, and it will redirect the user to text to speech with that prompt in the URL. So we kind of pre-fill it with initial value. So the handle generate button will do exactly that. It will trim the text so we can check if it's empty. And if it is, we will return. Otherwise, we're going to do router.push, open backticks, and we're going to redirect to text to speech and we will append a text param and we are going to make sure to encode URI components so nothing unsafe is passed like this. There we go. Perfect. Now let's go ahead and let's return a div and let's go ahead and start adding some class names to this div. let me go ahead and expand like this we're going to start with rounded 22 pixels followed by bg linear 185 then we're going to start the gradient from a specific hex color we're going to add from 15% via another hex color so this is one class name don't mind the collapse via will be 39% and then we're going to add two another hex color two will be 85% and let's go ahead and add a very subtle padding of 0.5 and shadow with 00004 pixels white like this. Then inside of here, we're going to go ahead and render a div with class name rounded 20 pixels and a very specific background. You might be wondering why am I getting this class name right here? Well, And also, how do I see the content of my Tailwind classes? How do I have these colors? Well, all of that is thanks to extensions. So first of all, make sure you have Tailwind extension on. Tailwind CSS IntelliSense. This will allow you to see the content behind the class names. And you can deduct if you have an invalid written class name, because when you hover, nothing appears. So every time you hover and see the content inside, you know that you have a properly written Tailwind class name. And you also get warnings like this. So rounded 20 pixels can be written as rounded for Excel. This is the one specific case where we don't want to do this. And I'm going to add a little comment why. So let me go ahead and open a comment here. So just so you understand why. using pixel values for border radius to ensure proper gradient border math. So I will try to demonstrate later what would happen if we used rounded for Excel, which is percentage based rather than 20 pixels. So I'm going to add a follow up comment here. Standard classes like rounded for Excel use, my apologies, not percentages. They use calc, which doesn't align cleanly at corners. you will very clearly see the difference so that's why we i'm forcing pixels here and not using for excel all right so that's me sharing some uh border knowledge with you here now inside of here let's open another div with a class name space y4 rounded to excel background color of white padding of for drop shadow extra small and now inside of this div we have the input area so let's go ahead and render the text area well I should have said text area well we don't need a comment for something that's obvious right let's go ahead and give this a placeholder start typing or paste your text here and let's give it a class name minimum height of 35 resize none border zero background transparent padding zero shadow none focus visible ring zero let's go ahead and follow it up with some attributes value text on change get the event and call set text with event target value and maximum length is text max length which we've defined previously so i want to render this already so we don't build any more without seeing uh what we are actually building so the place to the place to render this is in the dashboard view so let's go inside of dashboard views dashboard view and right below the dashboard header let's add text input panel and again you can choose how you want to import these I prefer explicitness and you can see the subtle effect so it's kind of gradient. You can see it even has this. It's a really, really cool design. And I think I can now demonstrate inside of text input panel, if we go here to round 20 pixels, if I change this to rounded for Excel, you can see what happens. You see how the border looks odd. So when you use pixel based, it will properly be calculated. But when it uses the tailwind for Excel, it uses calc, and then it messes up on the borders. So that's what I was trying to show you. All right. So now that we have the actual text area, we should go ahead and develop the bottom info so the bottom info will be a div with class name flex item center justify between a badge with variant outline class name gap 1.5 border dashed a coins icon rendered inside with size 3 and text chart 5 color. Then let's go ahead and render span with text extra small. And let's check if we have, let me just go ahead and create a proper ternary here. So if text length is empty, we are simply going to render start typing to estimate. But if it is not empty, we're going to return a fragment. And in here, we're going to go ahead and calculate using and calculate what the estimated cost of user's prompt will be. So we are going to multiply the text length, basically each character with 0003. That's because that is our price, which currently is just a magic number but later when we add billing with polar it will make sense because we will have usage-based pricing so for every character we're going to calculate how much this will cost and make sure to add the dollar sign at the beginning here so you can see now it says start typing to estimate and the more i type the bigger the number gets so it's using characters to calculate how much this will cost you. Let's go ahead and add an empty white space estimated. So they know this is an estimation. We cannot guarantee it won't be calculated in a different way. Now let's go outside of this badge and let's go ahead and render how many characters we had left. So text extra small, text muted foreground, and we're going to use text length to locale string, and we're going to use text max length to locale string characters. There we go. So let's go ahead and see that. There we go, zero out of 5,000 characters. and I think this should look good on mobile mode too. So let me switch to iPhone SE. I use iPhone SE as like the smallest possible device that I want to support and it looks good here. So I wanted to show you this because when I collapse it myself, it looks broken, right? But this is smaller, I mean thinner than the smallest device I want to support. So I don't really care about this example. I care about this, right? All right. And now let's go ahead and render the action bar. So let's go ahead and go after we close this span, after we close this div, and then another div, let's render the action bar. The action bar will be a div, flex items center, justify end, and padding three. and it's simply going to render a button. Size small, disabled if there is no text which is trimmed so you aren allowed to enter white space On handle generate which will redirect to text to speech with the text Class name with full lg with auto and generate speech So you can see now it's disabled until I start typing and it's full width on desktop on mobile but on desktop it transforms to a corner button. Beautiful, beautiful work. All that's left is to create quick actions, which are basically just redirects with some predefined prompts. So let's prepare with quick actions data. Quick actions data can be whatever you want. I'm going to show you an example. So let's go inside of source features dashboard. Let's open a new folder called data. And inside, let's go ahead and create quick-actions.ts. I'm going to go ahead and export an interface quick action, which is a title, description, gradient, and an href. And then I'm simply going to export an array of quick actions. So for example, the first one can be narrate a story with a description which can be something like bring characters to life with expressive AI narration. The gradient can then be something it needs to be proper tailwind so from CN400 to CN50 and the href would then be a text to speech redirect and then in here the content you want to out of fill so this can be something like this in a village tucked between mist covered blah blah blah so basically a prompt uh i suggest you just go inside of the source code and find features data quick actions uh and just copy this thing You know, I mean, this is just marketing gimmick. This is just some prompt suggestions that I generated with AI. You can generate yours with AI too. You know, you can just select this and tell it to generate five examples like this. Right. So that's exactly what I'm going to do. It makes no sense for you to write this by hand. It's important that you have a title, description, gradient, and Ahrefs and have six quick actions. Right. So I'm just going to add the rest. So I have guide a meditation, introduce your podcast, voice a game character, direct a movie scene, record an ad, and here is my missing semicolon. And I believe that's six. One, two, three, four, five, six. That's right. So just make sure you have six quick actions. Here, you can, you know, just add some placeholders if you have no idea, or just use AI to generate, a couple of examples. It's important that each href leads to forward slash text to speech. And since we're going to be using Next.js link, it will automatically encode this. So you don't have to worry about white space. I'm not sure if this is like the best way to do it, but it works. So we can leave it like this for now. It's a simple marketing landing page, right? All right. So once we have quick actions, we have to go ahead and actually develop these. So let's go ahead inside of components and let's develop quick actions panel dot t s x. And for now, all we can do is we can import those newly created quick actions. You can again use this or you can use features dashboard data quick actions. let's go ahead and export function quick actions panel and let's go ahead and return a div space y four and inside let's add a heading quick actions with text large and font semi bold and then let's create a grid with gap four let's go ahead and use two columns on medium devices and an extra large devices, let's use three columns. And then let's go ahead and iterate over our quick actions. And each action, let me just add another parenthesis here, each action should be rendered within a quick action card. So we don't have this yet, we're going to develop it in a moment. Make sure you pass in the key, title, description, gradient, and an href like this. So that's the quick actions panel. And now let's go ahead and develop the last component in this chapter, which is the quick action card. So the quick action card will have link from next link, arrow right from Lucid React, and let's go ahead and import type quick action from data quick actions, or if you want to be specific, you can always use this. and you can use type on the inside or on the outside if all of them are going to be types, which is the case for this one. So quick action is the type we define here, the interface more specifically. All right, now let's go ahead and also remap the quick action type to be quick action card props. And then we can just export function quick action card, map the props and have title, description, gradient and href because all of these are exact same props. So quick actions panel is passing the exact same props as we have inside of our quick action and quick action here. That's why we can share all of these components and their props. Inside of here, let's go ahead and start with a viv, flex gap four, rounded extra large, border, background card, and padding of three. And I want to stop right here and go back inside of Quick Actions panel and import Quick Action card from .slash Quick Action card. Or if you prefer explicitness, you can import from here. And then let's go ahead inside of Dashboard Views, Dashboard View. And let me just check the exact place to render this. all right so after text input panel quick actions panel and I like to be specific so I'm just going to go ahead and replace this import so you should now see quick actions text and you should see six empty boxes perhaps on desktop it's a bit more visible so they are very very light and now we will actually see the development of them so let's go back inside of the quick action card and let's let me just go ahead and find the component I'm developing so we're going to go ahead and create a visual placeholder with gradient now this is just to make it look pretty class name we'll use cn from lib utils so just make sure you add an import for that so now let's add the the static ones so that's going to be relative height 31 width 41 shrink 0 overflow hidden rounded extra large and background linear to this is bottom right all right and then let's add a comma and let's add gradient here gradient is our valid class name which we define in the quick actions here so that's why i told you these need to be valid tailwind classes so we can render them properly all right so let's continue working in the quick action card uh all right and now let's go ahead and add some decorative elements here so this is just going to be you know some fun design now absolute inset zero flex items center justify center and let's add div size 12 rounded full background white with 30% opacity. So this is one line background white with 30% opacity. And then let's go ahead outside of this div and just render another one to make it have another border. Absolute inset 2, rounded large, ring 2, ring inset, ring white with 20% opacity. and then we're going to go ahead outside of this div and we're going to render the content and the content will have a div flex flex call justify between and py of one it's going to have another div which is a space y one it will have a heading three element text small font medium rendering the title and beneath it there's going to be a paragraph with text extra small text muted foreground and leading relaxed rendering the actual description and then to wrap it all up outside of this div we're going to add a button it's going to have the following attributes variant of outline size of extra small class name of width fit and as child the as child is required because we're using link with an href prop and try now arrow right icon so i began explaining as child, we already had a scenario is required. So this button then becomes the href element. Otherwise, it will be semantically incorrect. All right. So let me just see if this looks okay. So this is a specific zoom level where it doesn't look perfect. But this is 100% zoom. So it looks normal here. But yeah, on 150 before it collapses into a two column grid, it looks a bit weird. So you can go ahead and do the following if you want to. You can go inside of quick actions panel and you can go ahead and decide when it should break. For example, I can use LG for grid calls three and everything else to be grid calls two.\nOr do I need a specific MD? Okay, so we need on medium, grid calls two. On large, grid calls three. And otherwise, grid calls one. I mean, this is by default, so I don't think you even need it. Maybe not the perfect solution. But yeah, you can play around with it until it matches the design you want. I think this mostly looks good. there's like one small edge case one specific zoom so what should this do when you click on it when you click on it it should redirect to text to speech which is a 404 but if you take a look at the url you should see the exact text and you can see it's properly uh encoded and the same thing should happen here so hi there my name is antonio and if i go ahead and click generate speech i I should see the exact same redirect to text to speech with encoded text. Hi there. My name is Antonio. Beautiful. So we have completely developed everything we envisioned. And we are now ready to create a pull request. So you can actually have proper CI CD in this. So we added 20 changes, some of them by hand, most of them by hand, But some of them are outside of our manual changes like components JSON, which added the AsEthernity registry because we added the way we background using the chat CN component. If you didn't, you won't have this change. So no worries about this, really. We did next config to hide the developer indicators. We installed some packages, which automatically modifies the log file. We added the logo. We deleted some test files. and then we just created all of these other ones beautiful beautiful work so this is what i want to do next i want to go ahead and check if we can build our project because i don't want to create any pull requests before that is possible that seems to work and let's do npm run lint which should work as well so warnings are fine errors are not so you can see we only have a warning in way we background because we properly fixed the S-Lint problems. Beautiful. So now let me go ahead and do git checkout dash b and in here I'm going to go ahead and call this zero two dashboard. so that's my branch name and then i'm going to do git add to stage all of these 20 changes you can also see how the branch has now changed here too there we go so now we stage those 20 changes and then i'm going to do git commit m02 dashboard and now those 20 changes have been committed And then I'm going to do git push u origin. And let me go ahead and do 02 dashboard. What this is going to do is it's going to push the branch to remote. Now let's go ahead and visit our GitHub repository. So when you visit your repository here, you should see 02 dashboard had recent pushes. And in case you can't see a button to create a pull request, you can always manually go inside of pull requests and click new pull request. The base should be your main and then you should select zero to dashboard. And then you can see all of the changes we just did. So let's click create a pull request right here. And let's just click create pull request. And then we're going to review our changes. As you can see, I'm using AI assistance to review my pull request. And in this specific example, it actually found some really important issues with the changes we've done. For example, I cannot pass up to 5000 characters without exceeding browser server limits. I also have an invalid query here, you can see the letter text is missing. I completely missed that, but CodeRabbit fixed the malformed query param. So what is CodeRabbit? CodeRabbit is, in short, an AI review tool. It is much more than that. The thing is, it understands the context of your project, unlike any other tool you can find. So it's much more than just an AI prompt, which says analyze the pull request. You will see how we're going to create more and more branches and pull requests, it will have a deeper and deeper understanding of the project. And using the link on the screen, you can actually go ahead and review everything it can do. You can try it for free with a two-click install and add it to your GitHub, and every subsequent pull request will be reviewed by CodeRabbit. Not only can it generate reviews like this, for example, a list of new features, we have added some chores, which we've done basically a summary for this pull request, it will also find serious issues in the project. So all these are minor right now. For example, this one is a major. This is a completely broken quick action link. But later on, when we develop some business logic, this is where it will come even more in handy. It has saved me countless time in previous projects where I introduced some accidental serious security issues and CodeRabbit caught them. So it's always a good idea to have another set of eyes take a look at your pull request even if you're developing solo. All right so yes this is something we will fix in another chapter. I'm not sure if you made the same mistake as me but yeah basically we have to lower the amount of characters we can push via the query string and I also have to fix the narrate a story one because it's missing the text param. You can see this one has question mark text but this one doesn't. So it's an invalid card, which CodeRabbit has noticed. Amazing job. So now let's go ahead and merge this pull request. And once we merge this pull request, it will count as a commit to main, which means that we now have Railway building our app again. So let's wait a second for that to rebuild. And one thing I forgot to enable, but basically Railway has an ability that every time you open a pull request, it can create a temporary preview of the pull request. So if you don't want to merge it to see it deployed, but want to see a preview URL, you can do that with Railway. So once you are in your project here, click on settings above and click on environments. In here, you have PR environments. By default, this is disabled. So you can just click enable PR environments and all future PR environments will be deployed. In here, it asks for the base environment. It basically means that variables from this environment will be used in the PR environment. So I'm going to select production because we have some important environment keys there. Now, obviously, you would go even deeper into separating the production, preview, and local. But for ease of use and for simplicity's sake, I will set up the settings like this. And we're going to see it in action in a future full request. For now, let's just confirm that Railway is able to build our current most recent change. And there we go. deployment has been successful and now if you go ahead and visit the production railway app you can see our most recent changes available right here beautiful everything is here let's check out desktop works beautifully amazing amazing job so that is our cicd process from now on every time we complete a feature we're going to go ahead and open a pull request the pull request will automatically create a preview URL from Railway, a review from CodeRabbit, and then we're going to merge that and safely keep track of all of our changes. Amazing, amazing job, and see you in the next chapter. In this chapter, we're going to create the text-to-speech page with a responsive three-panel layout, including the text input, the voice preview, and the settings sidebar. And then we're going to wire everything up with TanStack React form, adding four voice adjustment sliders. So creativity, voice variety, expression range, and natural flow, as well as the actual settings and history tab. All of this is going to be connected using TanStack form. So we're going to build it in the following order, starting with the layout and the three panel shell. So we're going to reuse our knowledge of layouts and pages to reuse this component page header, create this component, this component, and this layout and make everything look nice. And then we're actually going to make this sliders connect to a state as well as this input. And this will be interesting because these are completely separated components. and we need to find a way to use a single form instance for various detached components. And that will be a really cool exercise because we're going to have to learn context-based form hook and form provider to wrap all of these panels so they can communicate with each other and share each other's values. Excellent. So that is the goal for this chapter. so the last thing we did was and this is only relevant to you if you're actually following my git instructions right so if you're just building your own way you don't have to do this but in the last chapter we merged a pull request zero to dashboard right we have this summary by code rabbit and this is a great reminder for me because there's a mistake in my quick actions right so i merged this. Essentially what happened with our git is that our current state which is on branch 02 dashboard is identical to our main branch right there is no difference between 02 dashboard and the main branch this branch is not ahead of main branch in fact it behind main branch because the main branch now has the merge pull request commit so in order to start developing this and something we should have done in the previous chapter is simply check out back to main and then let's go ahead and do git pool origin main which will essentially just pull the merged state from the remote main branch. So this command which I just ran git pool origin main is essentially the same as if you go inside of your IDE here and select the main branch here and then click this synchronize changes. So that's what it does. Push, pull, and push. It's a synchronization. You can see that we are completely clean here. And if you want to make sure you're on the same page as me, you can open up the graph. So we started the app using Create Next app. That was the first commit. Then we added ShatZNUI, Clerk, and Prisma. That was the second commit, but we are marking that as one because that was the first change that we actually did. I then added an additional commit to fix the post install script, right? This was just a very simple package JSON post install script. And then what we did is we opened the 02 dashboard in another branch in a pull request. And then we merge that back to main. So your graph, if you followed my instructions, should look like this. What's important for you right now is that you make sure that you are on your main branch. Make sure you're not on any feature branch and just make sure that when you actually run npm run dev, you can go ahead and see the changes that we did in the previous chapter. If you can see that, you are good to go. Perfect. Usually, we're going to make sure that we do these changes at the end of each chapter so we don't have to remember to do it at the start of the next one all right so i'm gonna go ahead and immediately go inside of my source features and then dashboard data quick actions and in here i'm just going to fix the invalid href you might or might not have this bug so i am missing the text param here you can see that all of my other ones have it but i forgot one here great so code rabbit caught that bug. Amazing. Now what we have to do is we have to build the actual text to speech page layout. So let's start by doing the following. Let's go inside of source app folder and let's go ahead and go inside of the dashboard. So I'm going to add text to speech like this and then I'm going to add page.tsx inside and in here I'm going to go ahead and export default function text to speech page and I'm just going to return a heading element which will yield text to speech. So now if we go to localhost 3000 text to speech this one right here we should see that right here. Let's see. There we go. Text to speech. And one important thing to notice is that the sidebar is still available. The reason the sidebar is available is because we developed text to speech within the dashboard route group. If you actually move it outside, you don't have to do this. I'm just doing this, you know, to demonstrate it. If you actually moved it outside, you can see that text to speech doesn't have the sidebar. So make sure that you are developing your text-to-speech within Dashboard right here. And if you get this unsaved files, don't worry. Again, this is just cache. You can close this. You can click Save. Don't save. It does not matter. Just close this big .next folder. It's just cache. It will get regenerated. Don't worry about it. Now let's go inside of Text-to-Speech. If you wrote this, you can remove it because that's invalid syntax. And now what I want to do here is I want to add a reserved metadata here. So I'm going to import type metadata. Let me go ahead and import metadata from next. And then I can export const metadata. It's important that it's named this way. And this object will have a title of text to speech. So what happens then? If you look closely at your tab, you will see that now it has a text to speech label. But I want to make it just a little bit different. So I want to go ahead and go inside of source app folder layout. This is the root layout where we define the font, the body, the HTML. In here, we can do one cool thing. So we already have the metadata here. Perfect. So let's change the title here so it's no longer Create Next App. Instead, let's go ahead and let's just make it say Resonance. And for the description, it can be whatever you want. I'm going to add AI-powered text-to-speech and voice cloning platform. So now if I go to Dashboard, you can see it says Resonance. but if I go to my text to speech it just says text to speech but what I would want it to say is resonance actually I would want it to say text to speech pipe resonance that's what I would like it to do and technically we can do that by hand in each you know page that we develop right I can do text to speech pipe resonance but that's not exactly the proper way to do it the proper way would be to focus on this layout inside of the app folder and open an object instead of a string here give it a default of resonance but add a template and this template can use the following syntax percentage s and then pipe resonance and by doing this you can just add various different titles to your pages and notice the tab now text to speech resonance so now it's more of an industry standard this is the behavior you see in production apps i just wanted to show you that little trick. Great. So now that we have the layout, let's actually develop the layout view. So I'm going to go inside of source features. We already have text to speech and now I'm going to create views folder. And in here, I'm going to create text to speech layout.tsx. I'm going to go ahead and import a reusable component we developed the last time, which is page header. And I'm going to export function text to speech layout, which simply has a children prop. And I'm going to return a div with class name flex, full height, a height reset using minimum height of zero, flex column, and overflow hidden. I'm going to render the page header with a title text to speech. And beneath it, I'm going to render the children. This file by itself doesn't do anything because this is just a component. What we have to do now is we have to go inside of the app folder, dashboard, text to speech, and let's go ahead and actually create layout.tsx. Since it's empty, we now have an error. So what we have to do is we have to import the text to speech layout, and then we have to export default function layout. I'm just going to collapse this so it's easier to look at. So layout is a default export. Why is this a default export? Because it's in a reserved file name. So almost every file within the app folder requires a default export to work. That's how routing is recognized within Next.js. So what we are basically doing is instead of writing the layout here directly in layout.tsx, we are maintaining it and developing it inside of features text-to-speech views. So in here is where we write the actual content. all right so now that we have this you can see that we have a nice text to speech at the top and we can reuse it you know for voices voice cloning and everything else we have in the future perfect now that we have that let's go ahead and create the text to speech view so i'm going to go back here in features text to speech views and let's add text to speech view dot t s x text to speech view will have the following components so i'm just going to prepare these because they don't exist yet so we're going to have text input panel voice preview placeholder and the settings panel We don't have any of them, so I'm just going to comment it out so we can enable them later. And for now, you can just export a function text to speech view. The text to speech view is going to return a div with flex min height 0, flex 1 and overflow hidden. And then flex min height 0, flex 1 flex call, which is going to hold the text input panel. and the voice preview placeholder. And outside of that div, we're going to have the settings panel. So for now, we can comment these three out simply because none of them exist yet. So what this is, it's basically the same thing as text-to-speech layout. It's just a function, just a component, right? So it's not exactly rendered anywhere yet. So now we're going to go inside of our text to speech page and we're just going to return text to speech view like this So we should now have import text to speech view like this so we should now have import text to speech view and import type metadata and we're just returning this so notice the difference every time that we are within the app folder and we develop a layout we are using a default export when we are in a page we are using a default export. Here we have an exception because this is metadata, but I'm talking about the component, right? The JSX. It's required to do a default export. Otherwise, the component will not be recognized in routing system. Same is true for the dashboard route group. If I go inside of page, default. But if I go ahead and click within the dashboard view, you can see we don't do default exports. Nothing would really break if you changed the dashboard view or something like that to be a default export. You would just have to change the way you import it. But I don't like doing this because with default exports, you can name it whatever you want. I don't like that. I like to explicitly export without default and then I have to import like this. all right let's make sure that so far we only changed several files so right now you should have text-to-speech view text-to-speech layout if you had a mistake you will also have quick actions modified text-to-speech page text-to-speech layout and general layout perfect so now let's go ahead and let's develop the text input panel. I'm going to go inside of source, features, text to speech, and we're going to open up components. Inside of components, I'm going to add text input panel dot tss. I'm going to mark this as use clients. I'm going to import use state from React and coins icon from Lucid React. I'm going to add badge, button, and text area components from ChatCN. I'm going to import my text max length, which we defined in the previous chapter, actually. And I'm going to go ahead and start developing the text input panel. So again, this is a component. We're not within the app folder. So we can do an export function here. So we have a named export. Since we don't have any form libraries yet, we're just going to maintain this with a use state. So text and set text. Then let's go ahead and let's return a div with flex height full, minimum height of zero, flex call, and flex one. Now we're going to develop the text input area. So that will have a relative minimum height of zero and flex one. And within that container, we're going to render a text area component. So first let's add the controlled prop value which uses the text and on change appends the event target value to set text. Then let's go ahead and add a placeholder here. Start typing or paste your text here. And then let's go ahead and give it a class name. So the class name will have absolute inset 0, resize none, border 0. Then it's going to continue with background transparent, padding 4, padding bottom of 6. On a large, padding will be 6. Then we're going to add on large devices again, padding bottom will be 8. text is going to be base, leading is going to be relaxed, then we're going to have tracking, tight, shadow none, wrap, break, word. And last one is going to be focus visible ring zero. The maximum length of this text area will be equivalent to the constant we defined. so this is enough for us to render this so we can actually see what we are developing you can also pause the screen if you want to type out or confirm your class names and again if you have the tailwind css extension you can hover over your class names and if the definition pops up it's correct but if accidentally made a typo you can see that nothing pops up just to remind you of that trick so now let's go ahead inside of our text to speech views text to speech view let's uncomment text input panel and you can see how we can import it from dot dot components text input panel because we just have to exit the views folder and enter the components folder if you prefer you can also directly go with the features folder, whatever you like. And let's now uncomment text input panel inside of text to speech view. And just like that, you should be able to see the text start typing or paste your text here. You can see it almost looks like the entire thing is the text input, which is exactly the kind of look we want to achieve. Great. Now that we have that, let's go ahead and let's add a bottom fade overlay. So why do we need this exactly? Well, I think the best way to demonstrate is to kind of add a bunch of text here. Basically, we want to make sure that at the bottom here, you have kind of a fade. And the reason we need that is because we're going to have some buttons here and then those buttons will kind of overlap with the visual text right if you have a long line like this let me try and delete it yes for example the button will be right here and that just doesn't look good so let's go ahead and develop the bottom fade overlay which is just a single self-closed div this is it class name pointer events none absolute inset x0 bottom 0 height 8 background linear to top from background to transparent and you can see how now it's kind of subtly fading out it's very very subtle but you can see that when you have a bunch of text You can see how they almost disappear into a fog-like effect, I guess you could call it. That's the effect we want. It's a subtle change, but it makes it look much better. It doesn't seem so right now because we didn't actually develop the action bar, which we're going to do next. So almost before the entire thing ends, let's add an action bar. the action bar is going to have a div shrink zero padding four and enlarge padding six we're going to start with a mobile layout so while you're developing this i'm going to recommend being zoomed in so you're looking at the mobile version here okay so mobile layout is going to have a div with class name flex, flex column, gap three, and hidden on large devices. And we're very simply going to go ahead and render a button with a class name with full generate speech, right? So this is it. This is the mobile view. So you can go ahead and paste some text here and you can see how it doesn't abruptly ends. If you comment out the bottom fade overlay, you will see that it kind of gets cut out right which looks very bad uh when there's a lot of text maybe not so much on mobile but on desktop it looks even worse but with this simple class name you kind of make it like fade out into some kind of fog right it's just a subtle effect all right so that's it for the mobile view the mobile view is very simple actually uh so outside of this div right here we're now going to develop the desktop layout. Now the desktop layout will be different depending on if we have any text written or if we don't. So prepare a ternary like this. If text length is larger than zero, it means we have some text. Otherwise, let's go ahead and do this one first. So let's add a div which it will be hidden only visible on large devices on desktop and inside of that div a simple paragraph with a class name text small text muted foreground get started by typing or pasting text above and then inside of this one let's go ahead and start with a different div like this with a class name of hidden items center justify between and flex on large devices so if i clean this up now let me go ahead and zoom out you now have to be zoomed out so you can see the desktop mode but yes the goal is basically that there is no generate button until we start typing something well we don't even have the generate button on desktop right now but you should see this text when you don't type anything and then empty when you type something. So now let's go ahead and let's add a badge. A badge will have a variant of outline and the class name of gap 1.5 and border dashed. Badge is a component from chat CNUI which we've added when we began developing this. Beneath, I mean inside of the badge let's render the coins icon with class name size 3 and text chart 5. Beneath the coins icon we're going to render a span with an extra small text and then in here we're going to render another span with a class name tabular numbers. This is a super cool class name which is basically used whenever you have moving numbers. I will try and remove this class so you see the difference. Basically in here we are doing the exact same calculation that we did in another text input panel. You should have two text input panels. One inside of this feature you are developing right now, and another in source features dashboard. You can see that in here, we have the exact same function right It would probably be a good idea to separate this 0 into some form of constant Perhaps we can actually do that I think that would just make sense Let go inside of text to speech data constants, and I'm going to go ahead and do export const cost per unit, and I'm going to make it 0.0003, okay? And once you have cost per unit, let's go inside of uh well we need to modify both of our text input panels let's go ahead and first go inside of text to speech components text input panel so the one we are just developing and instead of this magic number let's go ahead and use cost per unit which we can import from data constants I like to use the explicit, but you can also use this if you prefer, whatever you like. All right, so you should have cost per unit and you should have text max length. Now let's go ahead and copy this and let's close the text to speech feature. Let's go inside of dashboard components, text input panel, and let's go ahead and add both cost per unit and the text max length. And let's change the magic number with cost per unit. This way, we don't have to worry about this changing in the future. All right. And nothing much should really change. on desktop when you start typing it's it's very zoomed out but you should be able to see the estimated cost calculation this will be easier to demonstrate once we add the bottom voice preview panel because right now this is just huge and if i try to zoom in okay i can i can work with this perfect but basically you can see that when i type something the i'm trying to demonstrate the badge expanding right but basically um if you don't use tabular nums class name it can shift the layout and it just looks very bad so if you go inside of features text to speech components text input panel and find this where you calculate the cost and remove tabular nums i'm not sure if this will exactly be a good example yeah i'm not sure if you can see but it's kind of twitching see you see how the border is going left and right but that doesn't happen if you enable that class name that's why we need it tabular nums and not many people know about that but it's like a perfect solution whenever you need to display that kind of data that changes you can see how now there's no shift at all right another cool trick i hope you just learned uh great let's go ahead and focus back on finishing this so we are in desktop layout and we just added the calculation and we replace the magic number with a constant. Now what we're going to do is we're going to go ahead and add an empty space here and write estimated like that. Then let's go outside of this badge and in here we're going to render a div and let's close it. The div will have a class name of flex, item center, and gap three. in here let's add a paragraph text extra small and tracking tight and i'm going to go ahead and render text.length to locale string and then i'm going to add a span which is text muted foreground and i'm going to go ahead and render a divider and we can render some empty space here like this and then let's go ahead and display text max length to locale string characters like this so now on desktop you should also have the amount of characters you have written you can see that right here all right so that works uh and if you want to represent white space in another way you can also use this unicode if you prefer that is the exact same effect perhaps your ide even recommends this because this can work differently depending on like if you add enter here or maybe add some spaces here right using a unicode might actually be more consistent so whatever you prefer actually for example i really like using those so i know the white space is expected here so it's an and sign n b sp and the semicolon okay and then after that paragraph let's render a button size small generate a speech and we finally have the desktop layout. So this is how it's going to look like when there's no text, except later this will not be a text, this will be prompt suggestions. We're going to suggest some pre-made prompts for the user. So they can start typing and then they will be able to generate the speech. All right, pretty good. So now that we have a text input panel, let's actually develop the voice preview placeholder. So again, instead of components, I'm going to add voice preview placeholder dot t s x. I'm going to add audio lines, book open sparkles and volume two from lucid react. And I'm going to add button from components UI button. Let's export function voice preview placeholder. And let's go ahead and develop this component. This will be just a UI component there won't be any logic here. So we're going to start with a div which is hidden on mobile and on large devices it's flex meaning visible. We're also going to have flex1, full height, flex call, items center, justify center, gap 6 and border top. Then in here let's go ahead and let's add flex flex call item center gap three and then let's go ahead and add another div relative flex width of 32 items center and justify center and in here we're going to add a div which encapsulates our icon so class name absolute left zero minus rotate 30 rounded full background muted and padding for and let's go ahead and render volume 2 icon with a class name size 5 text muted foreground let's stop here and let's actually render this so we're gonna go back inside of text to speech views text to speech view and let's uncomment the voice preview placeholder and you can import it from dot dot components voice preview placeholder or if you are like me and you prefer explicit import, you can use add features, text-to-speech components, voice preview placeholder. And let's uncomment its place. It's only visible on desktop, so you have to zoom out. And this is basically what it looks like. We're going to have three icons like this, and then we needed a text which says this is where the audio preview will appear. all right let me try and zoom out a bit so maybe i can see while i'm developing so let's go back instead of voice preview placeholder and unfortunately i have to collapse the screen even more i'm not sure if i can zoom out that much here uh so to make it easier to look at this perhaps you can add like spaces here so you understand that this is a div encapsulating an icon right so you can now copy this and paste it and this is the second icon in here we're going to have sparkles. And then let's go ahead and copy it again. And this is going to be audio lines. So both volume two sparkles, I mean, all three volume two sparkles and audio lines all have the same class name size five and text muted foreground, but their containers will have different class names. So we're finished with the volume two, but now we have to modify this one. so it's not going to be a left 2 we can remove that this one will actually be a relative we're not going to have rotate z index will be 10 rounded will be full background muted will be 4 that's all good and let's go ahead and change it from background muted actually to background foreground and let's go ahead and change the text to text background so it's reverse all right and then the audio lines one will not be aligned to left but to right and everything else I think is exactly the same so now let's take a look at what this looks and there we go so a nice little three icon layout perfect then beneath all that actually outside of this div let's go ahead and enter a paragraph preview will appear here and this paragraph has large text font semi-bold tracking tight and text foreground and beneath it we're going to have another paragraph once you generate your audio result will appear here sit back and relax let me go ahead and fix the unnecessary white space. This class name will have a maximum width of 64, text center, text small, and text muted foreground. And to wrap it all up, before we wrap this div, let's just add a button with a variant outline, size small, a book open icon, and don't know how button and this can have an app post like this. Let's see how it looks like. There we go. Preview will appear here. Once you generate your audio result will appear here. Sit back and relax. And you can go ahead and try adding a bunch of things now. And you can see how this fade now looks much better, right? It doesn't have a clear cut out line here. So that's what I wanted to do that. Great. So we now have that and what should we do?\nthis button do? Well, usually it will lead to your documentation, right? So you can import link from next link. Let me show you, make sure you have link from next link like that. And the Ahrefs will be, you know, your documentation, or you can just use mail to and add your support email if you don't have anything in place right now. And make sure to add as a child so this button becomes the link. so at least now the button has a purpose it's gonna open an email but yeah in production you should probably make this go to some kind of documentation all right now let's go ahead and develop the settings panel which is the last shell layout component we are building so instead of text-to-speech components let's go ahead and add settings-panel.tsx I'm gonna go ahead and import settings from lucid react i'm going to export function settings panel and i'm going to go ahead and return inside of here we're going to render a div the div is going to have a hidden class name so it's hidden on mobile it's going to have a width of 105 which calculates to 420 pixels minimum height of zero which is basically a height reset flex call border l and on large devices it will be flex basically hidden on mobile flex on desktop or invisible on mobile visible on desktop with flex properties in here we're going to go ahead and add another div with flex items center gap 2 border bottom px4 and height of 12 let's go ahead and let's render settings icon with class name size 4 beneath it let's go ahead and render a span with small text and font medium rendering the text settings outside of the div encapsulating those two let's render another div with flex flex one items center justify center and padding four and the paragraph which renders voice settings will appear here the paragraph will have text small and text muted foreground and that's actually it for the settings panel right now. So we can go ahead inside of text to speech views, text to speech view, and we can uncomment the settings panel. Again, if you prefer, you can go ahead and import from this. I just really like being explicit. And we can render the settings panel. What's important is that settings panel is not rendered inside of the same div as text input panel and voice preview placeholder as that's going to break the entire layout. And you can zoom out and there we go. This is the three panel layout, the text input, the voice preview, and the settings bar exactly as we envisioned. So that is part one done. We now have to go ahead and add a tan stack form and develop these sliders and actually connect the entire form functionality and this value functionality so that when we click generate, we can actually submit all of those information from various different panels, which are detached from one another. So this is where we're going to have to kind of learn how to pass values, form values through context. So before we go ahead and add the tan stack react form, I just want to make sure we are on the same page. So I have 11 changes in here and I'm just going to go ahead and let you pause the screen so you can see all the changes I have done. Again, you don't need to have all the changes as me. For example, quick actions. I had a bug with a missing text param. You might not have that. Perhaps you didn't modify the layout if you are not interested in the title thingies. But just go ahead and make sure that you didn't forget any crucial new files which were created. All right, if you confirm everything is fine, let's go ahead and let's do npm install tan stack react form. We're going to use this along with the field component from chat CN UI. And usually, the implementation you will see me do now for tan stack React form is usually not as complicated. But since we are working with a very, very specific layout where our form is actually in this component and in this component, we need to do it in a very specific way. So we're going to go ahead and learn how to use form in a context way, right? What I'm trying to say is UiShatsen has TanStack form documentation here. And you can see how they intend to use it, right? You can see it's very simple. You have your use form and default values, validators, nothing you haven't seen before, right? Ours will be a little bit more complicated. That's what I'm trying to say. We will have some things like this. we will have filled label and we will have form.fill thing is but the problem is sharing the use form value across different components right that's what the goal is so now that you've installed that what i like to do is i like to go inside of packet json simply so you are aware what version i am on if you're on a drastically different version like version 5 it might be a good idea to look at the breaking changes, or you can simply go ahead and use npm install tan stacked form with exact version that I am using. Great. Once you've confirmed you have the package, let's go ahead and create a new file within source hooks. So in here, we only have use mobile, which we didn't develop, we got it from Shotzi and UI. But this one we will develop use app form, which will be used to create forms which span through multiple components. So from the new package, tanStackReactForm, let's import createFormHookContext and createFormHook. And from this createFormHookContext, let me go ahead and fix this. We can actually import fieldContext, form context, use field context, and use form context. And then from create form hook, we can also extract the following. Actually, create form hook accepts the field context, form context field components and form components so these two are empty right just an empty object and from here we can extract use app form and use typed app form context and once we have those uh we are ready right uh we are basically initializing this once so we don't have to do in all components that will need this. All right. So now what we're going to do is we're going to go inside of features, text to speech, data folder, and we're going to add sliders.ts. So what is a slider? A slider is basically this creativity, voice variety, expression range, and natural flow. And while we are going to present this to the user in a natural language, human readable way, the way we are going to store this is pretty dependent on Chatterbox text-to-speech parameters. So Chatterbox text-to-speech is our AI model, which we're going to learn how to self-host and they have very specific I mean the model has very specific parameters it accepts so we're now going to create types for that so we map it correctly so each slider can be the following temperature top p top k and repetition penalty now I would advise you to carefully write this correctly casing is important but even if you write it incorrectly, don't worry, we will have type safe APIs. So you will eventually see a mistake and a bug if you made any typos here, which is a good thing because then you will know exactly what to fix, right? But try to do it correctly from the start so you reduce the amount of problems. All right, so I'm just adding all the properties here and then we're going to go and just list them so we're going to have id label left label right label minimum value maximum value increment step and the default value and then we can go ahead and create various sliders so we're going to have creativity voice variety expression range and natural flow so for example let's add temperature id which we are going to label as creativity the left label will be consistent the right label will be expressive minimum will be zero maximum will be two step will be 0.1 default value will be 0.8 so i didn't invent these numbers these are actually inferred from the chatterbox dts model which we're going to develop later. If you already want to explore this, I'm going to show you where you can do that. Using the link on the screen, you can see Chatterbox text-to-speech GitHub repository. And in here, if you want to, you can go ahead and read more about how it works. And you can see it's code even. And this is basically where I have obtained these sliders and values from. So just to make you aware that these are not magic numbers and they will have more sense later once we actually have a Python server which accepts those values. But I just want to make you aware of the GitHub repository which we are going to use. all right so if you want you can also visit my source code and go directly into text-to-speech data sliders.ts and then copy the entire file because you won't really learn much by typing this out by hand and you have higher chances of making an error but however you prefer so we just did id temperature which we will translate to the users as creativity of the model And now we going to do a top P value which we will present to users as a voice variety ranging from stable all the way to dynamic, minimum zero, maximum one, step of 0.05, and a default value 0.95. Then we're going to add top k, which we're going to express to the users as expression range, ranging from subtle to dramatic, minimum of one, maximum of 10,000, step 100, default value of 1000. and then the last one will be repetition penalty which we're going to name natural flow ranging from rhythmic to varied with a minimum of one maximum of two step 0.1 and default value of 1.2 i'm just going to slowly go over this so you can pause at any moment to confirm with your code or as i said you can simply go inside of the source code and look at it so now let's go ahead inside of text to speech components and let's create text to speech form dot t s x let's go ahead and mark this as use client let's import zod and form options from tan stack react form and z from zod let's import our use app form from hooks use app form which we have developed uh where was i here i am use app form perfect and then let's go ahead and develop the text to speech form schema using zod. So open up a zod object and let's first require the user to enter a text with a minimum length of one otherwise they get an error please enter some text. Secondly they're gonna have to select a voice id from the database so please select a voice and then they're gonna have to select the parameters which aren't required and won't throw errors because we will have default values for them. So those are temperature, top p, top k, and repetition penalty. Great. From that object we can create a type text to speech form values using zod infer type of tts form schema. And then using that type we can create constants like default text to speech values. In here, I'm going to assign the text to be empty, voice ID to be empty, temperature to be 0.8, top P to be 0.95, top K to be 8000, and repetition penalty to be 1.2. These are the default values that Chatterbox TTS model will pass if you don't even pass these values, right? So if you don't add them, these are going to be the defaults anyway. So that's why I chose those. So users can at least visually see what the defaults are. Then let's export const text-to-speech form options using form options from TanStack React form and passing along the default values. And then from here, we can go ahead and export function text-to-speech form. text to speech form will have two props children and optional default values which are text to speech form values then let's go ahead and define the form by using use app form let's go ahead and open an object within this hook and let's start by spreading the text to speech form options we defined above, which infers the default values. Now, we still have to pass the actual default values here, which we can do by the following. If the component has default values passed, we're going to use them. Otherwise, we're going to use default text-to-speech values. So what's the difference? These default values from the prop are often going to be history, like an older generation that users selected. So we populate the exact properties that existed at the time, right? And if that isn't passed, so basically if default values exist, it means we are revisiting an existing generation, whereas this means this is a new generation we are trying to create. So the validators are going to be on the onSubmit event, and we're going to validate by using the text-to-speech form schema ZOD object. And the onSubmit method will simply be an asynchronous function with generation logic to be added later on. And for the return, we simply add form, app form, render the children inside. As simple as that, we now have a reusable text-to-speech form with context, default values, validation that we can share across these layouts which we have created. So let's go ahead and continue and let's create another component here called generateButton.tsx. We're going to go ahead and mark this as useClient and importButton and spinner from ShatsNUI. Let's go ahead and export function generateButton. Let's go ahead and prepare the types and props here. So generateButton will accept the size disabled, is submitting, on submit, and class name. The props for that, I mean the types for that will be size is an optional which can either be default or small, disabled is a required boolean, is submitting is a required boolean, on submit is a required function, and class name is an optional string. And then in here, we're going to go ahead and return a button component like this. And we're going to add it the following class names, size, class name, on click, matching to on submit, and disabled. We're then going to check if we are submitting, we're going to render one thing. Otherwise, we're going to render another thing. So if we are submitting, let's go ahead and render a fragment. Whoops. A fragment with a spinner class name size 3 and the text generating. Otherwise, let's simply render generate speech label. Great. So we are writing this small component and we are purposely doing that because we are going to reuse it in two places. So it just makes no sense to have this exact function twice. since we can easily reuse it now. Now let's go inside of components and let's create settings-panel-settings.tsx. In here, I'm going to mark it as use client and I'm going to import use store from Tanstack React form. Then I'm going to go ahead and import field, field group and field label from components UI field followed by slider component. from components UI slider. So all of these are coming from chat CN UI. We've added them in the first chapter when we ran npx chat CN add dash dash all. And then I'm going to add use type app form context from hooks use app form. This is what we developed recently, right? So you should definitely have use type app form context use app form and all of these. Let me go back to where was I? Settings panel settings. We're then going to import the constants for the sliders from dash dash data sliders or features text to speech data sliders. And then I'm going to import text to speech form options from text to speech form or again whatever you prefer. I like seeing exactly what feature something belongs to. So yeah, if you want to, you can keep it much simpler. Just .forward slash text to speech form. All right. Now that we have text to speech form options, let's actually develop the function settings panel settings. So why such a weird name? Well, because this panel here is considered the settings panel. And the settings panel can either show history or settings. So depending on what the user clicks, we're going to display something different here. So if the user clicks on settings, we should show the settings panel settings. If the user clicks on history, we're going to show the settings panel history. So unfortunate name, perhaps I could have thought of something better. But just to explain why such a weird name. Let's go ahead and extract the form into this component by using use type app form context and passing along the text to speech form options. And then from here, we can also extract is submitting by calling use store, selecting form.store, and specifically selecting s.is submitting. so now let's go ahead and return the ui so we can actually see it we're going to wrap this instead of an empty fragment first we're going to do a voice style drop down drop down section so this actually doesn't exist so for now let's just do border bottom border dashed and padding four and let's add a paragraph voice selector coming soon with a class name text small and text muted foreground all right and then outside of that div let's do voice adjustments section in here we're going to go ahead and render a div with a class name padding 4 and flex 1 and then we're going to do the field composition so we're going to add a field group with class name gap 8 so we added field group field label and field right here make sure you have that along with the slider And what we going to do is we going to map over sliders And for each slider we going to go ahead and render form field So let go ahead and render form dot field with a key slider dot ID and the name slider dot ID. And then in here, we can go ahead and do the following. So we can extract the children property like this and get the field we can render that within the field component for each slider we can render slider dot label within field label and then in here we're just going to go ahead and make it pretty so we're going to add a div with a class name flex items center and justify between and the first thing we're going to display here is a span slider left label meaning the the minimum range right so let me close everything else so in sliders this would be consistent right and then on the right end it will say expressive so if you pull the slider all the way here it's consistent if you pull it here it's expressive that's what we're trying to render now all right so that's the left label and then next to it another span right label identical class names and we are using justify between, so there will be a bunch of space between them, right? All right, and then outside of this div, we have to render the slider. The slider is going to have a value which is held within an array like this. See, it's within an array. That's because the Shatzian slider component can store multiple values. But for our use case, we're just going to be working with a single value. So you have to put it within an array. Just be careful you do that. Okay. Then let's go ahead and add on value change to get the value and call field handle change and get the only or the first element from the value. So again, the exact same reason we need to do this is the exact same reason we had to do this because slider is naturally multi-value but we are only using one value from it so that's why we have to extract the first value from this value prop see and then let's go ahead and just pass along other props minimum slider minimum maximum slider maximum step slider step and disabled if the entire form is submitting so if the user clicks generate we're going to automatically disable all of these That's why we had to develop this context-based form hook so the form provider can wrap all of these panels. We can initiate form submission from this component and it will immediately react into this component. That's why we had to do it in a more complicated way. And if you're confused about this, like field group, form.field, field, like what is this? Well, this is just chat CN composition. I think we already developed something like that form, tan stack form. You can see those examples right here. Maybe not exactly this one. But yeah, you can see this prop. Children field, that is the same thing as doing this. I could have technically done it within a prop, but it's just cleaner to write JSX not within a prop, but the other way. I think they definitely have some examples of doing it in a different way. Perhaps they can. Okay, so they seem to be doing it this way right now. But yeah, you can do it in multiple ways. I suggest you read this page a little bit so you can see various ways this can be used with. Okay. So now what we have to do is we have to add some class name to this slider just to make it look a little bit better. don't worry it's not going to be too much it's just they are big class names right so the first class name is the following asterisk asterisk column data dash slot slider thumb size three so yes it has to be specifically like this no spaces then the second one is exactly the same prefix so you can copy this part and the only difference is after the semicolon we do background foreground and then the last one is slightly different so again asterix asterix data dash but instead of slider thumb it's slider track height one that's it that's the class name and I'm going to zoom out just so you can see how they all look like so this is one class name this is second and this is third okay perfect now that we have that let's go ahead and develop the settings panel history so settings panel history but we're not actually going to develop anything in here we're just going to add a placeholder state so import audio lines audio waveform and clock and you can go ahead and export function settings panel history here let's go ahead and return and what we're going to develop here is awfully similar to voice preview placeholder right so we're gonna have three icons like this but just in a smaller arrangement so i'm gonna go ahead and add the first div here flex height full flex column items center justify center gap two and padding eight then we're gonna have another div with a relative flex width of 25 item center and justify center and then we're going to render our three icons so the first icon is going to be audio lines and its div will have absolute left zero minus rotate 30 rounded full background muted and padding three the icon itself will have size four and text muted foreground then for the second item we're gonna have audio waveform with relative z index of 10 rounded full bg foreground padding 3 and audio waveform size 4 and text background so the reverse the exact same thing we had to do here but we're just doing it with a smaller width and i think some sizes are a little bit different and then the last one is identical to the first one except positioned on the right side instead of left side so absolute right zero rotate 30 rounded full background unit padding three clock icon size for text muted foreground and then outside of this div let's add a paragraph no generations yet font semi bold tracking tight and text foreground and last one generate some audio and it will appear here maximum width of 48 text center text extra small and text muted foreground uh great so now let's go ahead and actually uh learn how to render those two components right because we now have those two components settings panel history and settings panel settings but the actual settings panel is just this right so we have to change that So let us start by removing the import and let's instead add history and settings from Lucid React. Then let's go ahead and let's add tabs. Let's add tabs content, tabs list, tabs trigger from components, UI tabs. then we're going to go ahead and add settings panel history from dot slash settings panel history and settings panel settings from settings panel well settings all right then inside of the actual settings panel here we can delete everything within the parent div so just leave hidden with 105 minimum height of zero flex column border left and large flex and in here we're going to go ahead and render the tabs element so the tabs will have a default value of settings and a class name flex height full minimum height of zero flex column and gap y of zero inside we're going to have tabs list. Tabs list will have tabs trigger. The tabs trigger will have a value of settings and in here we're going to render settings icon with a class name size 4 which will say settings like this and then we can copy that tab trigger and change this one to be history and the text to be history so i think already we should see that there we go so we have settings in history okay not looking perfect but they are visible and what we have to do now is outside of the tabs list but still within the tabs we have to add tabs content and in here let's render settings panel settings let's give the first tabs content value of settings so that's what this one is controlling let's give it a class name margin top zero flex minimum height of zero flex one flex column overflow y auto like that okay looks like something has happened here so for now i'm just going to comment this out okay and let me duplicate the tabs content here paste it and change this to be a value of history and in here we're going to render settings panel history okay and for for this we can just put a paragraph settings panel all right let expand so now it settings panel but if i click on history it should change to that but it not So let see exactly what we did wrong here I didn change the value The value should be history Okay. Default value is settings. So that's what's going to be selected first. And when you click on history, it changes to no generations yet. Generate some audio and it will appear here. All right. So now let's go ahead and quickly deduct what is happening with settings panel settings. When I uncomment this, we get an error that we aren't wrapping. Okay, so I think we need to wrap our app within the form component somewhere. I'm just not entirely sure where. So let me see. Just a second. so we have to do it inside of text to speech view okay so go ahead and delete this paragraph settings panel and make sure you render settings panel settings and go inside of text to speech views text to speech view and now we have to modify this a bit so we're going to go ahead and let's see let's mark this as use client first that's very important and then we're going to go ahead and import text to speech form and default dts values from components text to speech form like that and then we're gonna go ahead and wrap the entire thing around text to speech form like this okay let me go ahead and try and invent this and let's go ahead and pass in the default values to be default TTS values. There we go. So by doing this, that should be fixed. I'm going to change this to go features text to speech like this, and then I'm going to keep them together. And now, there we go. You can see our sliders, creativity, consistent, expressive, voice variety, the stable, dynamic, expression range, subtle or dramatic, natural, flow, rhythmic or varied. And it changes depending on the tab we are clicking on. And the voice selector coming soon because we didn't develop it just yet. Perfect. So I just want to make sure we fix that error before we continue here. Because what we have to do now for the tabs is add some class names. so let's go ahead and develop the class name uh yes here in the settings panel let's go ahead and develop the class name within a constant tab trigger class name so this will be a relatively long one so feel free to copy it from my source code if you want to flex one height full gap two background transparent rounded none border x zero border top zero border bottom pixel border bottom transparent shadow none data state active border bottom foreground so this is a single class name this isn't separated it needs to be all together without any spaces then this is a long one group data dash variant default forward slash tabs list data state active shadow none super long one so it's important that you understand this is all one class name there's no space here okay this doesn't end this collapses so you can see the entire code without me having to scroll here all right same thing with this this is also one class name all right once we have the tab trigger class name let's go ahead and find the tab trigger here and here and let's go ahead and give it a class name and pass in the tab trigger class name great we have that and I think we did all other class names here so let me see it's definitely not perfect not what I intended so let me try and find exactly what I did wrong here. Flex, height full, minimum height zero, flex call, yep, one. Oh, tabs list, okay. So tabs list is missing a class name with full, background, transparent, rounded, none, border, bottom, height 12, group data, orientation, horizontal, and square brackets forward slash tabs height 12 and padding 0 all right another super long one so group data orientation horizontal tabs height 12 i'm just gonna double check that i didn't misspell anything so feel free to copy this from the source code too but looks like everything is fine okay and now they look as i intended them to look perfect uh amazing so you can see that since we are able to move these values uh it means that the form is connected the form values are actually changing right now uh you could even try it to see if it works by modifying the default TTS values. If you want to, you can change everything to zero. And then you will see when you refresh that they are actually loaded. So this way you can confirm that the values are actually loaded. Make sure to reset them afterwards. All right. So we are almost done. What we have to do now is we have to go inside of text-to-speech components text input panel, because right now this isn't connected to the form. This has its own text here. We no longer need this. We no longer need useState from React. What we do need instead is useStore from 10Stack React form. We no longer need the button. We can remove that. But what we do need is useTypedAppFormContext from adhooks use app form. And we also need TTS form options from .text to speech form. And we need generate button from generate button. Again, you can replace both of these with features text to speech components if you prefer it this way. So make sure you have those. and now let's go ahead and fix some things here so instead of that previously written use state it's now going to be form use typed app form context we can get the value of the text using use store form store get the selector selector values dot text and you can see it's all properly written you can see how we can now share all the form values and validation in different components which are not next to each other and which are not nested nearly so that's how you do that when you have complex layouts like ours let's add is submitting the same way and let's add is valid the same way perfect so now we're going to go ahead and do the following around the text area here. So I'm going to add form dot field name text because that's the form field we're going to control with this. And let's go ahead and just encapsulate the text area. That's the only thing we need. Let's indent it. In here, we're going to add the children prop, which can be written like this and that will render the text area okay and then the on change here will be modified to call field on handle change my apologies handle change and event target value and the value here will be replaced with field state value so just like that we turn this into a controlled component which will be disabled if is submitting as well. All right. And now we have to go down here and we have to change from using this button in the mobile layout to using generate button like this. Class name will be full with disabled is submitting is submitting is submitting. on submit form handle submit okay and we're gonna have the exact same thing in the desktop layout so again find the error button let's go ahead and remove it now paste this and you can remove the class name and you can change the size to be small like this and i believe that is it so we now have a fully controlled form here so everything now works i think there might be a way to test if it actually works uh should we use is valid somewhere yes we should use in that is valid here in the desktop variant so it should be disabled if is submitting or if not is valid that should be the case okay uh for mobile you don't have to do it because ux is different on mobile okay so if it's not valid uh well it makes no sense now because we hide these but later yeah maybe we don't even need that is valid thing because we obviously don't even show if it's not valid uh right now the only thing that happens is this gets disabled because we're not even submitting anything. We're not even loading anything. But what we could potentially do is go inside of text-to-speech form and go inside of on submit. I'm not sure if we can extract the values from here. Can I try and getting the values just to see if that works? So let me go ahead and try hello world. And I'm going to go ahead and purposely mess around with these. Maybe I won't even be able to submit. Yes, I'm not able to submit because it\nhave the voice selector. I think that's why I can't even try it out. But since we are able to change the default values and see that that works, I think we are good to go. All right. So I believe those are all the changes that we need. So as you can see at the end of this chapter, I have 19 changes. If you want, you can pause the screen to confirm you aren't missing any important ones as i said multiplies during this chapter you don't have to have all of these funds you know depending on if you made the same mistakes that i did uh perfect so what we have to do now is you know just make sure everything works you know don't go forward if something is broken your cost estimation should work your character accounting should be working uh you should be able to switch forms you should be able to switch tabs uh it should look good on mobile too right i I mean, this is the only thing you should see right now. And one thing I'm interested about is text input panel in text to speech here. So when I have text, okay, so it's derived from here. So all the calculations are done using this text. Okay, that's all I wanted to know. Perfect. So what we're going to do now is we're going to open a pull request. So chapter three, text to speech UI. let's go ahead and do first of all let's do npm run lint to confirm that we can still build this that we didn't break anything looks good npm run build let's wait a second to see if it builds looking good everything passes if yours doesn't i would suggest fixing any errors that you have let's go ahead and do git checkout dash b zero three text to speech ui git add which basically stages all the changes and git commit 03 text to speech UI and then git push u origin 03 text to speech UI. There we go. Our branch is now on GitHub so let's go ahead and go to our repository here. We can now open a pull request and let's review our changes to see if we made any mistakes. And after a few moments, you can see that both our continuous integration and continuous development, thanks to a railway, has successfully passed. So this preview of this pull request is now actually available. So if you were developing within a theme, you could easily share your new changes which are deployed. So take a look at the URL, resonance, resonance PR to railway. So this is actually deployed on railway and you can share with your team or with whom you whomever you are developing this the progress of the app. And if you don't have this, it means that you didn't enable it. So if you go inside of your project on railway, go inside of settings, go instead of environments, go ahead and click enable PR environments. And for base environment, select production. And then it will apply to all future pull requests. And this environment, this temporary URL will be cleaned up when the PR is closed. So a super cool feature from railway. And this way you can keep track that whatever you just developed actually works in production. Brilliant. So we confirmed that. And now we have some changes and summaries from CodeRabbit. So new features. We added complete text-to-speech page with integrated form interface, text input panel with real-time characters, voice settings panel, everything we already know. Brilliant. Here are some comments in here. It's telling us to add an explicit type to this generate button. but we actually don't need to do that because we are explicitly using this button to submit. So if anything, we can give it a type of submit. Next, inside of here, it told me that we have an invalid border bottom pixel and it's actually incorrect. And the cool thing about CodeRabbit is that you can update its learning. So if you respond border bottom pixel is a standard tailwind utility in a newer version, it will add those learnings. In here, it's telling me that placeholder is not a substitute for an accessible label. So yes, we are missing accessibility in this text area, and that's definitely something we could improve here. In here, it's telling me what we actually noticed ourselves is that the generate button in the mobile layout is not using the is valid guard in comparison to desktop layout, which is using. So I just responded mobile version needs different user experience, and it added that learning too. In here, it's telling us that the voice ID is required, but it defaults to this, but the voice selector doesn't exist. So this is preventing the form from submitting. We are aware of this, obviously, because we are in the middle of a tutorial. But yes, if this was a real pull request, it would have been a broken pull request because you cannot submit the form because voice ID is always going to be empty because you cannot select it. Other than that, amazing, amazing job. let's go ahead and merge all of these changes this time we didn't have any major mistakes and once you have merged those changes we're going to go ahead and immediately do what we had to do in the beginning of this chapter which is check out the back to main and get pool origin main because branch 3 and main are now on the same level right because we just merged it so just confirm that you have two closed pull requests. This is the most recent one, meaning we just merged all of those changes. Go ahead inside of your IDE, confirm you're on your main branch, and you can always open source control graph to confirm this is what it looks like. So after we merged dashboard pull request, we opened up a new branch, text-to-speech, and we merged that back to main. Brilliant. And you can confirm if you have text to speech, you should have components, views that will let you know that you are on the correct branch. You should have all of that visible in the main branch. Great. So I believe that marks the end of this chapter. We developed all of this and see you in the next one where we are actually going to add. Well, we're not exactly going to be continuing to work on text-to-speech. We are going to add trpc and r2 storage simply so we prepare the ability to continue developing this beyond UI. Amazing job and see you in the next chapter. In this chapter we're going to set up the entire backend layer that powers the application. We're going to build a type safe API using trpc cloud file storage with Cloudflare R2 which is actually any S3 compatible storage. So if you just heard Cloudflare and thought oh but I want to use AWS no worries you can any S3 compatible storage will work but I will teach you how to set up Cloudflare R2 because of its free tier and simplicity of use. We're also going to build a seed script that is going to add 20 system voices to our Postgres database and also upload their samples to R2. So we are going to have a respective reference key to the uploaded element in our database. So it's all going to be nice and synchronized. We're also going to learn how to create auth aware procedures, such as base procedure, which will be used for public endpoints, out procedure, which will be used when something needs to be authorized, and organization procedure, which is very important because our app is multi-tenant by default. So you will learn how to do that. And this is the actual ALF layer too. So whenever you think about security in your app, it should always be within the data access layer rather than the middleware. In the first chapter, we worked with a proxy.ts file, which basically just serves as a nice redirect for users, right? If unlocked user attempts to open the app, they are going to get redirected to sign in page. But that's not actual security. We shouldn't rely on that proxy file. Instead, we should rely on things like this, data access layer security. So the key concepts to learn from this chapter are going to be type safe APIs, ALF middleware, server prefetching and hydration in client components, signed URLs for audio, and a seed script. The build order, which we're going to use, is first setting up TRPC, followed by Cloudflare R2 Storage, and finally developing the actual seed script. So let's get started by setting up TRPC. Make sure you have your app running here, and this should be the last thing we built, right? We built the history placeholder, the settings tab, we turned this into a better UI, and we actually have a form which cannot be submitted at the moment because a voice selector is missing. But we cannot build the voice selector until we actually have some voices. So let's go ahead and you can use the link on the screen to visit the TRPC IO page. And I want to show you exactly where you can find the snippets that I'm going to use to set up TRPC. So we can go inside of the documentation here. and in here you can go ahead and open client usage and in here you're going to see two react queries one is tan stack react query with a star icon and one is react query integration classic we're going to use the new one the star icon and in here select server components this is the guide that you need now in case you are watching this into future and something has changed don't worry the project is open source you can always just copy and paste exact components i'm adding and i will of course go over all the snippets we going to copy and paste so we will understand what we doing Great So let start by actually installing all of these packages here I going to go ahead and copy this and I'm going to install at trpc server, at trpc client, at trpc tanstack react query, at tanstack react query at latest, zod, client only, and server only. I'm going to go ahead and install all of these packages and you can see from my documentation that I'm currently on a version 11.x so my major version is 11. I would highly advise that you work on the same version as me and I'm going to show you exactly what version that is now that my package json is updated so let's take a look so 10 stack react query is 5.90.21 all of my trpc ones are the same so 11.10. 0.0 and my client only this isn't important this is 0.0.1 and this server only is also 0.0.1 so i would suggest to try and keep trpc at the same major version as me so this one doesn't really matter in fact when i developed this app the first time i think it was on eight or nine so yeah you don't have to match 100 great so that was step one adding all the packages so now let's go ahead and build a sample backend i'm gonna go ahead and zoom this in so we're gonna go ahead inside of source folder and in here we're gonna create a new folder called trpc and in here i'm gonna go ahead and create init.ds and i'm gonna go ahead and copy the contents of this file. So let's go ahead and I'm just going to zoom out just a bit. So we're going to import the RPC from the RPC server cache from React. We're going to build create the RPC context using that cache and just return mock user ID. We're then going to go ahead and create the TRPC instance here using init trpc.create and we're going to export create TRPC router, create color factory, and base procedure. You will also notice a commented out data transformer here. We're going to come to this later. So for now, this file is good as is. Let's go ahead and continue copying the snippets. So the next thing we need is to create a routers folder, and let's create underscore app.ts. So we're doing that inside of the TRPC folder. I'm going to go ahead and copy everything. So what we're doing here is we are importing Zod from Zod, and we are importing base procedure and create trpc router from our recently created init file. In here, we are defining the base app router. So what this is, is essentially a hello API route, which is using a base procedure, meaning it's public for everyone to use. The hello API route requires a param called text, which is a type of string. And it will return a object which holds a greeting key whose property is hello. And then it's going to use whatever we sent into the params. So it's basically a very type safe way to write your API endpoints. and combined with Prisma, which is already extremely type safe, it's going to make a brilliant developer experience and much safer code in the end result. All of the files which I'm going to create now, you don't really have to know at the top of your head, right? You will simply get to know them by using them. When I learned TRPC, I didn't really bother with understanding every single thing that's being written here. It just became clear after I had to use them. and I would recommend you doing the same. So this is mostly just following documentation. The reason I'm telling this is because I don't want you to get scared of adding TRPC, which is a relatively abstract concept. It's simply a way for us to write API routes in a very, very type-safe way. So the end result is important, not the way we are building this. In fact, this could have been an MPX TRPC in its script, if you ask me. But let's go ahead and add it one by one. So at least we are aware of all the things we have added. All right. So now we have to actually add a trpc to our Next.js API routes. The way you write API routes inside of Next.js natively, so not with the RPC, is by using a reserved file name, route.ts. so it works the same way as for example page.tsx except it's a route.ts so you need to create a folder we're going to create a folder called api and then we're going to go ahead and create trpc inside then we're going to create a dynamic trpc folder so this basically means anything and then we're going to finally add route.ts so this will be an equivalent of localhost 3000 forward slash API, trpc, and anything in between. That's how we are going to communicate with trpc throughout Next.js. So let's go ahead and add everything in here. And here we need to do some slight modifications. So we are importing fetch request handler from trpc servers adapters fetch, and then create a trpc context and app router from seemingly incorrect files. That's because they are using a different import alias. So just change this to an add sign and that should work because trpc, add trpc is referring to this. This is add trpc and we already have the init file and we already have the routers app file and that's exactly the ones we are targeting here and we also have the matching exports. So everything should work just fine. this endpoint should be left as is just make sure you didn't misspell anything so api trpc api trpc if you accidentally write tprc that's wrong that wouldn't work so make sure it's trpc this one actually doesn't matter this is just the it's important that it's in square brackets but do make it make sense you know so keep it trpc here as well all right so now that we have that let's go ahead and continue with scaffolding trpc again i repeat you don't have to understand every line of code at the moment because this is mostly just setting up a react query to work with trpc you only set this up once and you never think about it again so that's why i'm telling you that you don't need to you know worry about not understanding everything right now how you use it is more important than how we are setting it up because we are literally following the official documentation. So make sure you've created a trpc query-client. Let's copy it and let's paste it. In here, I have an error and that's because of a missing data transformer import. So I'm just going to comment it out for now. All right. So import super JSON is commented out. Serialized data is commented out and this is commented out. Great. So if you really are wondering what are all these options, you can read from the documentation to understand it better. Now let's go ahead and define the client instance of trpc. So that's different from query client. Query client is for connecting tanstack react query. This file client.tsx is a client instance of trpc. So I'm going to go ahead and copy all the content from the example and I'm going to paste it here. So this is basically the provider. This is what we're going to have to wrap our app with to make it available to use with the RPC. All right. I'm not going to go through all of the code here, but if you want to, you can pause to, you know, see exactly everything that's happening. There is one thing I want to change though. So this code right now seems to be looking for Vercel URL, and otherwise it falls back to localhost 3000. So that's fine for our development mode, but it will break in production because we are currently on railway. So I want to show you a slight modification that you can do, which is actually, this will be a good change even if you deploy on Vercel. It's going to be equally good. So go inside of .environment file here and at the top I'm going to add a new environment variable app URL and in here I'm going to keep it at localhost 3000 like this so just don't misspell it right you can go ahead and see after you do npm run dev you can see your exact protocol and your app so you can copy from here and paste it here okay and once you have that you can also go inside of source lib environment.ts and you can go ahead and add app url here and make that required okay so once you have this you can then go ahead inside of query client my apologies inside of client.tsx and in here uh actually i just told you to add it to .environment. I'm sorry, to this one, to an environment file, because I was thinking of using it. But here's how we're going to leverage this. I still recommend adding it here, simply so you get a warning in case it's missing. But maybe it's not that important, because technically, we do have a fallback here to localhost. So here's what I'm going to do. I'm going to still use process.environment here, but I'm going to check for app URL. And then if I do have an app URL, I'm just going to return process environment app URL. That's it. Otherwise fall back to localhost 3000. You can see how simpler this is to understand because if you use the Vercel URL part, it doesn't return the protocol, right? So you have to match the protocol and then append that. It's just weird. App URL is just simpler regardless of where you deploy. You're just going to have to remember to add the app URL variable. All right. So again, just make sure that you have the app URL variable set. Great. Now that we have that we no longer have to do anything else here but we do have to mount the provider in the root of our application so let go ahead and do that so this component name is called trpc react provider so I going to go ahead inside of source app folder layout.tsx and in here we're going to go ahead and import trpc react provider from at trpc client so make sure you're importing it from this component we just modified and once you have that let's go ahead inside of clerk provider so keep that as the utmost provider and simply wrap the rest of your app within the rpc provider great now that is it for the client side setup of the rpc but now we need to create server side setup so that's also going to be server.tsx yes and in here i'm going to copy the entire content and paste it here. I'm immediately going to comment this part out because you can see it's a suggestion. If your router is on a separate server, pass a client. Our router is not on a separate server. Let's just comment this out. Okay. So what I don't want to lean too much into explaining this file right now because I just feel it's going to confuse you. This is just setup and this basically allows us to use trpc within server components that's the uh lightest explanation i'm going to give okay so just make sure you're following the documentation make sure you're doing it in the proper folder with the proper file name and yes the extension is .tsx because in a moment we're going to add some react components here so if you've accidentally named it .ts change it to TSX because we're going to have to do that anyway. Okay, so I'm going to go ahead and skip a few things here because this is what I want to do. So let me show you exactly how to find this in the documentation. So this is the last part we did, the rpc server.tsx. And basically they say, okay, you're finished. You can now use the RPC. That's true. But I want to scroll a bit down until you can find this tip. And in here, it tells you to extend your server.tsx for a very good reason, because we are simply going to use this so many times that it makes sense to create a helper from it. So I'm going to go ahead and copy this part first. I'm going to add it at the bottom here. So here it is, hydrate client. And we need to import hydration boundary from tan stack react query. and dehydrate from tan stack react query so let me show you exact two imports i've just added it's these two okay dehydrate and hydration boundary from tan stack react query perfect and you can see once i've done that there are no errors and the second function we need to copy is the prefetch function so in here there is a bunch of type errors here honestly my recommendation is just to click quick fix and disable this for the entire file. And in here, cannot find TRPC query options. I think we can import that from TRPC TanStack React query. There we go. So let me show you that import. Here it is. So we already had create TRPC proxy options proxy, and now we've added TRPC query options. I'm going to repeat it one more time. I know I'm being annoying at this point but I don't want you to be intimidated by this I know this is a lot of code changes but this is a type of code that is usually scaffolded for you with some npx create next app or npx you know create backend but we've done it manually this time simply because I don't know if any npx trpc exists but it's also a good exercise to kind of learn how to do things manually but none of this should really bother you so much that you give up on the project, right? I've shown you the most important changes. You can pause the screen to see if you have all the files. If you do, everything should be fine. And even if you think you did something wrong, the code is open source. You can always, you know, just open up this file and see if you should change something. We will not change anything in this, this, this, this file. The only file we're going to modify this is where we're going to write our API endpoints. All right. So we have that done. And basically what that allows us, what we've just did with those two functions, prefetch and hydrate client function, is to use it in a very simple way. You can see how prefetch is now super lightweight and hydrate client is super lightweight as well. Whereas previously, you can see that prefetch needed to have a query client and then you needed to call prefetch query and then you needed to pass this hydration required a hydration boundary a state an import from the hydrate and you needed to pass a query client from above so you can see the simplification this was before and this where is it and this is now so much simpler okay uh great i believe we are finished when it comes to setting up trpc So what I want to do now is test if the RPC is actually working. So the way we're going to do that is by going inside of source, app folder, dashboard. I'm going to go ahead and open a new folder called test. And in here, I'm going to create a component called healthcheck.tsx. This is just a component, right? So we're going to pretend that this is our usual view component, because you can see that's kind of the structure we like to use. Whenever we create a page, we import a view, and this view is use client. So I'm kind of trying to replicate that exact scenario. So let's go inside of test, health check, mark it as use client. And in here, I'm going to import use suspense query from tan stack react query. And I'm going to import use trpc from trpc client. and then I'm going to export a function health check and I'm going to go ahead and return a div and just a paragraph health check. So let's go ahead and spin up our app to make sure everything is still running as intended. So let me go ahead and check if everything's running. I can close this tab. If you want you can restart your server just in case any cache isn't left and if I go ahead and go to help dash check so let me show you in my url well vocal host 3000 help dash check not found did i name it differently i didn't name it that i named it test my apologies localhost 3000 test that's what i named it and i still can't find that okay because i never created a page file. So let's go ahead and create a page file before we can test if this works. So this one is a default export. And let's return a very simple help check. There we go. So make sure that this is a default export and the health check is just a normal component. So there we go, health check text. So most of these things still work as intended. So what I want to do now is I want to create a simple trpc procedure to check the health of trpc. So we are going to go inside of trpc, routers, underscore app, and I'm going to remove the hello procedure entirely. And I'm going to add a health procedure, which will be base procedure dot query. So it can either be a mutation or query. It can be even more things, I think. It can be a subscription too. But we're going to only focus on two of them. Query is your API get request. Mutation is your post, past, put, all of those others. So query asynchronous and let's return status. okay great now let's go ahead we can remove zod from here no need once we have that let's go ahead inside of components my apologies add folder dashboard test health check and in here we are going to add the trpc hook so use trpc and then we're going to get the data from use suspense query trpc.health.query options and execute that and you can hover over data and you can see that the response is status string. That's the power of TRPC. So if I go ahead and add code 123 here, you can see that immediately this data now has that option too. So that's how TRPC works. It is end-to-end type safety. Now combine that with our access to Prisma and our type safe data models. Every time we return something, we will be absolutely sure that what we are working with on the front end truly exists in the API and on the back end. And this scene, I mean, we just spent 25 minutes adding this, but that's a one time change, right? Usually you will just copy and paste this from any other project that you have, right? But I'm trying to explain what we're doing here. one hint in case all of your types are any so you don't see the actual content the rpc has a little fix for that too if you scroll down i think it's extra information frequently asked questions here it is it doesn't work i'm getting any everywhere you can go ahead and pause the screen and see if anything here works for me this was the one that fixed it the first time I had this problem and I've never had it since if you don't have any problem no worries but you can see that they actually recommend committing this file to your repository so if you're working in a team all of you have the same trpc experience great so we can now close the trpc page we no longer need that and now what I want to do here is simply check if the RPC works So I going to go ahead and render the following So a div with class name rounded large border padding six text center text muted foreground text small, margin top, text large, font semi bold. Great. So you can see that I can reach the RPC. But as I refresh, you can sometimes see an error. So what exactly is happening here? Well, what's happening is that we are using use suspense query here, but we are never actually prefetching. So you should use use suspense query only in combination of prefetching the data in a server component. That's why we created both instances of client and server. So in here, what we're going to do is we're going to prefetch from trpc server, trpc, again from trpc server, dot help dot query options like this but that's not enough so make sure you've imported prefetch and trpc from trpc server because what we have to do now is the following we have to go ahead and add hydrate client which again you can import from trpc server and then in here i'm just going to go ahead and add some slight styling here. So I'm going to add a div, which has flex, flex column, item center, justify center, gap four, padding eight. Then I'm just going to go ahead and add a heading trpc test page with text to Excel and font bold. And I'm going to go ahead and do a suspense which you can import from react so add a suspense and add fallback here div loading like this so let's go ahead and test this out you can see for a brief second it says loading and then it's okay uh you can use command shift r to do a cache reset because if you just do command And R, you can barely see the loading, right? Because React query has some cache, right? So this basically points to everything working fine. So how about we break it? So let's go ahead inside of tRPC help. You can command click into here to get into the base procedure, which is end tRPC routers app. So how about we break this? Let's see. you can add this comment here throw new error something went wrong so now you can see it's stuck at an infinite loading and it will be stuck until it retries three times until finally we get an error that it ended up rejecting so how do you take care of that well you can use next js's error page but you can also if you want to be more granular you can use react error boundary so i'm going to go ahead and install that package now so npm install react error boundary let's just wait a second for this to be installed i'm going to show you the version i'm using this one is very rarely changed it's just a simple wrapper react error boundary 6111 and inside of page.tsx I'm going to wrap my entire suspense within an error boundary and I'm going to import error boundary from react error boundary and in here i'm gonna add a fallback for when something goes wrong so it has a similar syntax as suspense that's why i like it and it's a well-maintained library too it's not some random library and refresh now and you will see a similar thing so loading is now uh well loading because it's retrying when it fails it's gonna retry and then finally it reaches a state of something went wrong. So we're going to use every page.tsx file to prefetch the data, making it faster than usually just doing it with use query in a client component, hydrating the data with keeping track of errors and loadings with error boundary and suspense. And finally, in health check, we're just going to use a normal use client component. and from here we can call on click, we can call various hooks, we can do pagination, we can do infinite load, basically all of those things that are very difficult and awkward to do in a server component. So every time I see someone doing that in a server component or trying to do as many things as possible in a server component, I have to cringe because server components to me are very good if they're used in what they are made of, what they're made for. And in my opinion, I just think of them as API routes which are able to return JSX. They are not as simple, of course. I'm simplifying for the sake of understanding how I want to use them. And on top of that, they have access to the database directly. There is no middleman between a server component and your database. It's the same as calling your database within an API. So that's why I want to leverage a server component to query my database, prefetch it, and populate React Query's cache. So when it's time for a client component to use that data, thanks to use Suspense Query, it's going to be available immediately. That is basically the logic I'm trying to do. And I think I kind of explained that here. So client components will use trpchook and then use Suspense Query, server components will prefetch and hydrate, which will lead to API TRPC endpoint, which has the TRPC router, which finally has access to our Postgres database. That is the flow we have just implemented. And of course, let's now go back here. Let's comment this out. So refresh and it should be working. And if you want to play, you can also add a little loading comment here. So I'm going to uncomment this now. So just await a fake timeout and go ahead and refresh. And then you will see loading happening for five seconds, just in case you wanted to test loading. Great. That is basically how we add a TRPC to our project. That is how we're going to call data from now on. And mutations too. Basically, from now on, everything becomes way more type safe, way more secure, and way easier to work with. You will simply be reliant on your code. You will trust your code more. And if you're working with AI, it's easier for your AI to work with code because of extreme type safety. So your AI absolutely knows what they are getting. Perfect. Great, great job on that. So I believe that is the end of first part. The second part is R2. This is actually an easy part. There isn't too much code here. It's mostly setting up an account. In order to set up Cloudflare R2, the process is actually as similar as setting up AWS S3. That is because they are both S3 compatible storages. So let's go ahead and install the following packages. Instead of installing R2 specific libraries, we're going to add aws-sdk-client-s3 and aws-sdk-s3-request-pre-signer. So let's go ahead and install those two packages. I'm going to go ahead and prepare my package JSON here so we can see the change. They seem to have been added. Here they are. So these are my versions. it seems to be important that both of them are using the same version. So just keep that in mind. All right. Now let's go ahead and create our R2 instance or S3 instance. You'll see what I'm talking about. So inside of lib, I'm going to create R2.ts. And even if you want to use S3, you absolutely can. You will see the setup is exactly the same, but I would highly recommend just doing exactly what I do for the first iteration and then change it later once you have probably tested so that it works, right? So we're going to go ahead and import S3 client put, get, and delete object commands from AWS client S3. We're then going to add get signed URL from AWS SDK, S3 request pre-signer, and then let's go ahead and import our environment library so we can keep track of all the environment keys we're going to need. So I'm going to go ahead and initiate a new S3 client. So I'm going to call it R2 because we are connecting to Cloudflare R2. region is going to be auto and my endpoint is going to be the following i'm going to go ahead and add back text and i will simply make an https my account id dot r2 dot cloudflare storage dot com of course we are going to confirm that this is still valid why do i say still valid for me it's 100% valid, but maybe if you're watching it in the future, something has changed. So I will show you exactly where you can get this URL so you know it's correct one. And then you need to add the credentials. The credentials are access key ID and secret access key. We're going to store all of them under R2 access key ID, R2 secret access key. So these are the three commands, I mean environment keys which we're going to have to add. Now let's go ahead and define a type upload audio options which has a buffer a key and optional content type. Now let's go ahead and implement upload audio function. So upload audio function is an asynchronous function which accepts the buffer key and content type which is default audio forward slash wav. So we're going to map the options we typed above, and we simply return a promise. Inside of this function, we're going to await r2 instance.send, and we're going to initiate\na new put object command. The put object command has the following properties, bucket, key, body, and content type. The bucket is another R2 environment key we're going to have to add. So it's good that all of these are erroring right now because we will add them later so we know we didn't misspell them. Then let's add delete audio function. It simply accepts a key of uploaded instance and is again going to send something using r2 instance and what it's going to send is the delete command and for that it only needs a bucket and a key be mindful of capitalization of keys right and then let's add get signed audio url get signed audio url only accepts the key which is a string, and it has a get object command with the same bucket name and key. But instead of calling this from R2 instance, we're using get signed URL from this package right here. And we pass in the R2 instance, we pass in the command, and we pass in the expires at, which is one hour. Great. So now that we have all of these, let's add them to the environment file. So I'm going to go inside of the environment file right here. And I'm going to start by adding the R2 account, which needs to be required. Then I'm going to add R2 access key ID. Then I'm going to add R2 secret access key. And I'm going to add R2 bucket name. Great. And you can see that immediately, these now no longer error. So that's how you know that you've written all of them correctly. Except we didn't really add any of them to the environment file. So let's go ahead and do that now. We don't really have any info to add right now. So what I'm just going to do is I'm going to go ahead and just prepare empty ones. Let me see if I can do all of these at once. There we go. So account ID, access key ID, secret access key, and bucket name. So in order to set up Cloudflare, you can use the link on the screen to visit cloudflare.com. Go ahead and create an account and simply log in. Something like this will probably be your dashboard's homepage, but you don't have to do any of those. you can immediately go to storage and databases and find R2 object storage and go ahead and click on overview and I wanted you to see this screen simply so you know that we have a free tier credit card is required but you can also use PayPal and you will pay exactly zero dollars there isn't even an initial charge or anything you can see that they have a very very generous free monthly tier so 10 gigabits a month for free which is more than enough for what we are going to do with this so you have to go ahead and either add your card or paypal but you will not be charged at all once you add your credit card or paypal you can you will see this page right here so this is a completely empty r2 object storage and down here you can find the first key which we need which is the account id and this is what i was telling you this is how you check if your api has changed or not so in my case it's the exact structure i expected my account id dot r2 dot cloudflare storage.com so first check the account id copy it and go ahead and add it here r2 account ID. Then let's go here and let's double check the endpoint. So HTTPS account ID R2 cloudflare storage.com. So just go ahead and double check that that's exactly what you see here. HTTPS account ID dot R2 dot cloudflare storage.com. If you see something different, perfectly fine. Just adjust this to work with that. If you're sure this will be a private repository, you can even copy this entire thing and just paste it in plain text here just be careful so i mean no one can do anything with your account id i'm pretty certain they still need the secret access key and all of that but still you know be careful all right and you should also have the manage button here and the manage button here should give you access to create account api token so let's hit create api token i'm gonna call this uh resonance because that's the name of our app and i'm gonna select admin read and write which is probably the most dangerous permission to give but it's way simpler to just you know develop the tutorial this way later i would suggest you know toning down the permissions of this token to only what it needs right the ttl is going to be forever and i'm not going to add the client ip address filtering again all of this would be good to configure in production. Just for a tutorial, this is perfectly fine. A super powered API token. All right. So now I will copy the token value, even though I didn't really use it, simply because this is for Cloudflare API. But I'm not sure if you will see this again. So because of that, I can also add R2 token. And I'm just going to add it here, even though we're not going to use it. Then we have the access key. So we can add R2 access key. And we have secret access key. So let's add secret access key too. All right. So let me check if we have everything we need. Looks like the only thing we don't have is the bucket name because we haven't created any buckets yet. Great. And once again, you can see your URL here. But looks like they have EU as well. So if for legal reasons, you need EU, this is how you have to modify the URL. For tutorial, I'm pretty sure you can just use this one. But if for your business, you need to use the EU one for legal reasons, you can, of course, just modify this to be so dot EU dot R2 dot Cloudflare dot com. OK, so now that we have all of that finished, we have to go ahead and click finish here. You should see your token. Make sure it's active. Admin read and write applied to all buckets. And then let's go ahead and create a bucket. I'm going to go ahead and give this a name of resonance. And let's call it voices, actually, resonance app. Let's call it something like that. I'm not sure if these are scoped to global buckets. For example, on AWS, names are unique globally. so if resonance app is taken you know just try to change it to something else and this can be automatic and this can be standard you don't have to modify anything here and then you can go ahead and copy the name of your bucket for me it's resonance app go inside of dot environment and paste it here here it is r2 bucket name and that's it for setting up r2 we're going to test it in the only part left in this chapter, which is the seed script, right? So for seed script, we're going to need a list of voices. I will provide you with this list. I will show you where I found it. And I will also show you an alternative of what you can do if for some reason you can't find any system voices because these are just audio files. They are nothing more than 10 to 30 seconds voice samples, right? So you could technically create all of them yourself and just rename them to 20 different ones. It really doesn't matter. All right. So that's what we have to do next. So before we add system voices to our project, I want to make sure we don't commit them, right? So, I mean, technically you can commit them. Nothing is stopping you from doing so. But it would be a good idea for you to add forward slash scripts system dash voices to your git ignore here. Because we will technically then be uploading all of those voices, which makes no sense because they're only used once to be added to your Cloudflare R2, only for the seed script. This is the only reason we are adding them in our repository, right? Now, I will commit them simply because I want you to have a reference for those voices in case you can't find them anywhere else. But I would highly suggest that you add this to gitignore, okay? So I will remove this now, but I would suggest that you don't. I would suggest you keep it. Okay. And then let's go ahead and see how we can find those voices. So using the link on the screen, you can say the exact source that I found it, which is from model documentation. This is the documentation of the platform on which we are going to learn how to self-host an AI model called Chatterbox text-to-speech. And for now, this documentation isn't really important because we're not doing this now. But if you scroll just a tiny bit down in the chapter define a container image, you can see we'll also use Chatterbox provided set of voice prompts, which you can download here. So that is where I found them. Now, I can't guarantee that this exact documentation page will exist in the future. So I will show you the, I mean, you can also, what I wanted to say, you can also use my source code and then simply go in the exact place I'm going to add those voices now. Those voices are from Resemble and Resemble are the creators of Chatterbox text-to-speech in case you wanted to know. Okay. So I'm going to go ahead and add those system voices now by creating a new folder in the root of my app, which I'm going to call scripts. And then I going to add system voices inside So these are the ones that you should have all the way from Aaron to Walter So those are the system voices you should have Now a quick reminder I didn really listen to every single voice sample here So if some of them say something offensive, you know, just be careful. I'm not really sure what's inside of them. There are various, various samples to be able to create different characters and voices. So I don't really know what's inside them. Now that we have system voices, we can go ahead and create the seed script to upload all of those system voices to our R2 Cloudflare. But before we just do that, let's go ahead and create those. Let's make our app aware of built in system voices. So I'm going to go ahead inside of source features, and I'm going to create a new folder called voices. And in here, I'm going to create a data folder, voice scoping, scoping dot TS. And I'm going to export, I'm going to add all of this, I'm going to show you the full list, don't worry here it is so it's this export constant canonical system voice names so all the way from Aaron to Walter now in case for whatever reason you're unable to add system voices to your project right if you can't find them in my github repository for some reason or if this url has changed, right? What you can do is literally just find a voice sample that's longer than 10 seconds. Try to aim like 20 seconds. And you can just copy it 20 times and just rename it to all of these values, right? Or you can just use three examples and then just leave three names here, okay? So basically what I'm doing now is I'm making sure that my voice scoping file has all of the names listed in this folder. So I would highly suggest that you check one by one that all of them are here, okay? Because it's going to be important for the seed script. If you want to, you can reduce this list in half, of course, right? If you want to keep just four of them, you can do that, and then you can just delete the rest. Okay. So once you do that, also, yes, the names are kind of important because of the seed script. Once we define that, I want to go back instead of source features voices data, and I want to add voice-categories.ts. and in here we're going to import type voice category from generated prisma client what that is referring to is our prisma schema so in here we have voice category audiobook conversational customer service all the way to corporate right and what we're going to do now is we're going to use that enum to create labels so we could technically programmatically create those labels but in case you want different user-facing text you can do it here so basically i'm creating an object which will map every enum from my prisma schema to a string so the only one that actually makes sense here is customer service which usually would have been written like this on the front end but now it's written like this. So this is just a remap, which makes sense to do because these are hard-coded built-in voices. There's no reason to do this programmatically. I mean, technically you could, but these are all the options that are going to exist. And then let's simply add one helper at the bottom, export const voice categories, which will simply do object keys, voice category labels as voice category and then an array. All right, so voice category from Prisma client. Great. And now what we have to do is we have to build the seed script. I would highly recommend opening my GitHub right now. You can see the link on the screen because this seed script isn't really going to teach you too much. What we're going to do with it is just make it iterate over every single item inside of our scripts system voices, upload it to Cloudflare R2 under a specific key, and also create equivalent database record. So each voice record has a reference to the uploaded file. Okay. So I'm going to go ahead inside of scripts. I will create a new file. seed system voices dot TS. All right. So my suggestion would be that you copy this entire file from my GitHub simply because it's not so much of a learning experience as much as it is functional. So now I'm just adding all the imports which we are going to need. At this point, we kind of need to recreate our Cloudflare R2 client. And we also need to recreate our environment instance. Because this scripts folder is outside of the source folder. So it doesn't have access to any of those things. Okay. I also need to import Prisma client type voice category from generated Prisma client. I need to import all of the canonical system voice names from source features, voices, data, voice scoping. And this is why I told you to double check that all of these names match exactly what you have in system voices. And yes, here's another tip. If for some reason you don't want system voices, you technically don't have to because we will have a way to clone our voices later. But the problem is you won't be able to test if your app works until then. So because of that, at least make an effort to have like three voices in here. Make sure you, you know, map those three voices here and then do the seeding with just those three voices. So you definitely don't need all 20, but the more you have, the better your experience will be with testing this okay and now we're going to find that system voices folder using path.join path their name directory name file url to path using node url import.meta.url so now we have the system voices directory now let's create the environment schema so we need our database url r2 account id key ID, access key, the bucket name. We can then parse our environment schema against process environment to confirm all of this works. We need to create a Prisma Postgres adapter. We need to define the Prisma client. We need to connect to R2 once again. So S3 client. Again, And this is important if yours is different. So if when you defined R2, if you put .eu here or something like that or something changed, you also need to change it here, right? So you can see we are recreating all of those things now. Then let's create an interface voice metadata. So it has a description category mapping to voice category and a language, which is a string. and now in here we're just going to go ahead and basically create a list of all the system voices we're going to add to our database for example Aaron is going to be a soothing income like a self-help audiobook narrator categories audiobook English US and if you didn't use the system voices I provided if you made your own you can basically just do this for the amount of voices that you have in your system voices or the amount of voices that you have defined in voice scoping, right? So all of that should match, okay? I keep repeating myself because I'm not sure how easy or hard it will be for you to obtain all of the system voices and if you fail to obtain them, I don't want you to feel discouraged into thinking you cannot complete the tutorial. You absolutely can. You can always just create your own .vav files and then copy them a few times, match them into voice scoping and then match them here. So now I'm going to add the rest of the list. So this is why if you have system voices as me, you should go inside of my source code and simply copy this entire list. So in here, I simply listened to some of the voices or I actually looked at the transcript of some of them. That's why I told you some of them are a bit explicit. So, you know, be careful where you listen to them. and I've matched what I think is the category and what I think is the language. Okay, so now we have the voices. Then let's go ahead and create this function, read system voice audio. Then let's go ahead and create an R2 function to upload system voice audio. Accepts key, buffer, content type, and simply sends the put object command. All right, we have that. And yeah, basically we will store each of these under name.vap, right? And now I'm going to create an asynchronous function to seed system voice. In here, I'm going to go ahead and read system voice audio by name. I will get the buffer and the content type. I'm going to attempt to find an existing Prisma voice in my database. If it already exists, we're going to do one thing. If it doesn't, we're going to do another thing. So in here, I'm going to go ahead and do the following if it exists. So we're going to define the R2 object key to be voices, system, and then the ID from the database. We're going to obtain the metadata using system voices metadata, which we have defined So we're going to find the matching voice, right? Andy, and then we have the metadata for Andy. Okay. Again, you can just copy this from the source code. Okay. Let me see where I was. Yes, we're going to find that, upload system voice audio and we going to update that voice in Prisma by adding it the object key and extending it with metadata All right Otherwise outside of this if clause let just go ahead and write meta here Then let's go ahead and create a new voice variant system. Organization ID is null. Check if we have meta and if we have added here. and then let's go ahead and do practically the same thing define the r2 object key which is voices system voice id and then let's go ahead and open a try and catch block here inside of try i'm going to go ahead and attempt to upload it so upload system voice audio using that object key and update the equivalent database record with the uploaded r2 object key and if it fails we're going to make sure to delete the system voice okay and we need to catch an error all right and then i'm just going to go ahead and define an asynchronous main function which will log that it's seeding that many voices, right? So canonical system names, whichever voices you have created here is gonna attempt to seed. And then for every single name inside of this list, it will attempt to call the function seed system voice, okay? So that's why I told you that if you use a different number of voices or different names, you have to make sure to adjust all of that within the voice scoping. All right, and then let's just go ahead and add this so we actually catch any errors if it happens and disconnect from Prisma in the end. Perfect. So that is our seed script. I would highly recommend copying this from the source code simply because it's so prone to errors. There's barely any type safety here. It's just a seed script, right? There are many different ways you can, of course, do this. And now let's go and add this seed script to our Prisma. Where is our Prisma config? Is it in source? Let me see. Prisma.config.ts. We're going to go ahead instead of migrations and we're going to add a seed script to be tsx scripts forward slash seed system voices dot ts. So make sure you didn't misspell scripts and you didn't misspell the name of the file here and here and you need to have tx installed which i think we already do tx here it is in dev dependencies if you don't just install it all right we should be able to test this now so i'm going to prepare the following way I'm going to go ahead and open my Cloudflare storage here. I'm going to go in storage, R2 object storage, overview. Here it is, a resonance app. Currently, zero objects inside. That's the first thing I'm going to do. The second thing I'm going to do is npm run npx Prisma Studio. That's what I'm going to run. So in here, I have my Prisma Studio. So currently, I have two voices. it would be a good idea to delete all of the voices inside so it's completely empty no generations no voices here and let's try and run the seed script so i'm very interested to see if this will work i'm going to go ahead and do npx prisma database seed this should trigger the Prisma config seed script here. So let's see if that's going to work or not. So it's running the command. This is a warning. That's fine. And here it is. Abigail, Anaya, Andy. It's basically going over each file and uploading them right now. So let's see if it works. And there we go. You can see that I have successfully uploaded all of the voices. System voice seed has been completed, the seed command has been executed. So the first thing I'm going to do is refresh my R2 here. And let's see, there we go. We have a folder called voices. And inside, we have another folder called system. And in here, we have very important IDs. And if you're wondering what these IDs are referring to, I think these are the same as the database. Let me check. So inside of your database, You can now refresh your voices. And yes, you can see that each of these voices now has a CUID. And we have mapped the name, the ID of the database to the uploaded file here in Cloudflare. And each of these ones have their own key. and we have mapped the R2 object key to each of the, well, database models, which are actually just voices, system, and then their very own ID. So if you want to check if this works, I recommend downloading one of the files and just listening if it sounds like one of the examples that you had. Great. And that is actually it. we are ready to open a pull request, review the changes and merge this. As I said, if you had any trouble with the system voices, you can of course create your own. So you can just record yourself for 20-30 seconds, try to make it clear audio and then, you know, just add yourself three or five times in here. Pick random names or just copy these ones and then go ahead inside of your features, voices, modify your voice scoping to only feature those names. And then in the seed system voices, go ahead and modify the system voice metadata to only feature those few names too. And if you've done that, everything should be working just fine. Great. Again, you can always visit the source code if you think you've made a mistake. Beautiful. So as I said, you probably don't have all of these vav files here simply because I told you to add this to your gitignore because it doesn't make sense for you to have that because it's only used once okay so I'm gonna go ahead and open a pull request now I will shut this down I'm gonna go ahead and see the name so chapter four git checkout dash B zero four back end infrastructure. All right. I'm going to stage everything and I'm going to commit zero four back end infrastructure. Perfect. And then I'm going to do git push u origin zero four back end infrastructure. Mine will take some time simply because it has to upload all of those audio files, but yours will be much faster. And after a successful push, you will see your branch as an available pull request right here. So let's go ahead and create a pull request and let's see the changes. And here we have the summary by CodeRabbit. We added system voices feature with persistent cloud storage support. We introduced application health check page for system monitoring. We implemented voice categorization system for organized voice selection and enhanced error handling with error boundary protection, referring to our test TRPC page. And we also had a successful deployment on the web, though nothing has really changed for us, right? We didn't do any UI changes, we simply added voices, right? But one thing that you should do, what we forgot to do, but seems like everything is fine. We forgot to run npm run build and we forgot to do npm run lint. So you can see I'm testing this right now and everything seems to be okay. And I know it's okay actually because a railway would fail otherwise. So if yours is failing, it could be because of something here. Great. So let's look at the actionable comments from CodeRabbit. So in here, we have wrapped the entire layout with the RPC React provider, and it's telling me that I should move it inside of body because clerk's recommended pattern is around HTML, but the RPC React provider should be moved inside body. I've checked the documentation, and the documentation simply says mount the provider in the root of your application. So it doesn't really specify where. Maybe that's in the classic integration. i'm not sure setup let me try and find if that's in the classic integration somewhere i don't really know but hours work as expected so unless it starts causing problems i'm not going to move it in here i forgot to comment out the five second delay but we will remove that endpoint anyway uh great so great comments from code rabbit here's something to think about if something goes wrong. One thing you should do if you have your app connected to Railway, go ahead and open it. And we actually have to update the variables. Yes, because we added new ones. Right now we are not using them from anything but the seed script, but we will use them later. So we should add app URL, R2 account ID, access key ID, secret access key, all of these. So I'm just going to go ahead and add these at the top. There we go. And since I can add spaces, I'm going to keep some things separated here. So skip environment validation, database URL, clerk secret key, next public clerk publishable key, app URL, and then R2 token bucket name, secret access key, access key ID, and account ID. Let's click update variables. And once you've done that, you should go ahead and look for the apply six changes and hit deploy. It will still work just as expected, but we will now guaranteed not have any problems with the next chapter, which will be developing the actual voice selector Great I believe that marks the end of this chapter We have oh we didn merge the pull request Let merge the pull request first uh this was successful great we can merge that and then i just gonna go back here get checkout main git pull origin main so we are up to date with everything but on our main branch i always like to confirm that by clicking inside of source control and opening up the graph. And here it is, backend infrastructure merged into the main branch. Brilliant. So I believe that marks the end of this chapter. We developed everything here. We learned a lot and see you in the next chapter. In this chapter, we're going to connect the seeded voices to the UI and set up an external text-to-speech API client. We're going to build a voice selector with a drop down, which is going to communicate to TRPC Voices Router that we are also going to create. Each voice is going to have an avatar and we're going to have grouped sections for team voices, which we are going to create ourselves. And when I say we, I mean users of the app, right? And we're going to have a separate section for built-in voices, which is a user friendly way of seeding of saying seeded voices right the ones we created in the previous chapter we're also going to learn how to self-host the actual text-to-speech clients so we will use chatterbox text-to-speech along with modal serverless gpu and that will use a fast api Python server without the generated TypeScript types using OpenAPI architecture. By the end, the settings panel right here will have a working voice selector, and we will be able to actually try out our self-hosted text-to-speech client. So let's go ahead and start with building the voice router. So those are going to be, these are going to be the key concepts. The TRPC voices router, because it's going to be the first custom router that we're building within TRPC. We're going to learn how to create a context for voices, because we're going to have to combine both team voices and built-in voices. We're going to learn how to use Dice Bear Collection for custom avatars. And we're going to, in production, or should I say in a real world example, show how to do the server prefetching, which we kind of explained in the last chapter, and how to do open API typed client using fast API in Python and how to host the Chatterbox text-to-speech model. So let's start with the Voices router using trpc and database querying, followed by creating the UI, which will render that, and finally generating the Chatterbox client. all right so make sure you are on your main branch make sure you have npm run dev running and this is the last thing we did on localhost 3000 forward slash test we build a trpc test page so the first thing we can actually do is get rid of that so let me zoom in just a bit and we're going to go inside of dashboard and we can remove the entire test page we can also go inside of trpc routers app folder and we can remove the entire thing from here too and instead what we can do is we can do voices and we can add voices router we can remove the base procedure and i'm just going to fix this import to be leaner there we go voices router currently doesn't exist that's fine let's go ahead and change this to go back to localhost 3000 so we can see our homepage. And now we're going to go ahead and actually develop the voices router. So we're going to go inside of routers here and we're going to create a new file, voices.typescript. Let's go ahead and add the imports which we are going to need. Zod trpc error from at trpc server. This is a library, not our internal trpc server. You can notice a subtle difference. When we are referring to our trpc folder and then this server, we use another slash here. See, but that throws an error. So this is a package and this is our local trpc server. Prisma from database and delete audio helper from our recently created R2 instance. All right, then let's go ahead and simply export const voices router using create trpc router, which I have seemingly forgot to import, the most important thing. And let's also import organization procedure, which is another thing we don't really have. So we're going to have to add that. Okay, so I'm going to go ahead and go back to underscore app folder, and I'm going to import voices router from forward slash voices. So the out procedure and the organization procedure and base procedure are something we were supposed to do in the TRPC chapter, but I have seemingly forgot to do that. So let's go ahead and do it now. Perhaps this is better because during the tRPC setup, we were very, you know, confused with all the setup and it was too much information at once. So maybe it's better that we do it now. Let's go inside a tRPC init file. So in here, we have something called a base procedure. A base procedure means a public procedure. Everyone can access it and everyone can query it. So now we're going to go ahead and do the following. I'm going to go ahead and clear up my trpc context. For a simple reason, I don't need this, right? I'm just going to go ahead and make it be this. In fact, we can make it even easier. We can just make it an empty object. If you want to, you can turn it, you can leave it like this if you want to. Simply if you want to see this documentation because we copied this from TRPC. So basically, this is what it was. If you want to, you can just return an empty object. I mean, this won't change the behavior at all because we're not using this context for anything. So perhaps I should answer, why not implement alf here? Well, for a simple reason, in order to check whether user is logged in, we need to query clerks await alf. And it makes no sense to do that in here because this context is run for every single procedure, right? So it makes no sense to add additional overhead to both base procedures and to out procedure, right? And additionally, we are going to need organization ID too. So we would be adding additional overhead to load that. So because of that, context is not something we are interested in right now. So you can just go ahead and make it return an empty object. Then let's go ahead and go down here. So this is base procedure. And using base procedure, we're going to learn how to build an authenticated procedure. So this will call out only when needed. it. Let's export const out procedure. t.procedure.use async. Let's go ahead and open a function. And let's go ahead and extract next from the props. It's important that this eventually returns next. You can see that that fixes all of the issues. This is basically a middleware. A middleware always needs to go next, right? A middleware is in the end request. And from here, what we're going to do is we're going to extract user ID and organization ID from a waitout, which we can import from clerk next JS server. And then we're just going to do some checks. If there is no user ID, we're going to throw a trpc error. We can import that from at trpc server. Again, this is a package, right? And with a code unauthorized. And then we're just going to go ahead and extend the context with user ID, which needs to exist, and organization ID, which doesn't need to exist, but still it's useful to check at least if we have it, right? I mean, if you want to be strict, you can do this, right? And then you will never ever rely on organization ID using out procedure. Instead, if you ever need an organization procedure, an organization, you will build an organization procedure, which requires both user ID and organization ID. So it is very similar. You go ahead and export const org procedure, which calls t.procedure.use. Async function extracts next. Go ahead and immediately return it, so you fix the errors. and in here we're going to go ahead and again extract user id and organization id and we're going to do a check for a missing user id but we're also going to do a check for a missing organization id in this case we are forbidden not unauthorized but forbidden from moving forward so organization required because if a user doesn't have an organization there is no way for us to confirm that this user, even though they are logged in, how do we know whose organization do they belong to? That's why we need that. And let's go ahead and extend the context object. There we go. So that's what we wanted to do before. And while we are here, because I'm certain I will forget, we're going to enable transformers. Transformers basically allow us to parse data. specifically date map set over the wire between server and client right basically it's very hard to explain what it is until you see it in action but the problem is if you don't enable these data transformers you will start to see some errors between when you pass props from server component to a client component most of the time this\nIt doesn't really matter to us because of the way we are architecturing our app, but still, it doesn't hurt us to enable this. So that's exactly what I'm going to do. Let's go inside of the init file and let's enable SuperJSON. Let's go ahead and do npm install SuperJSON. While we are here, we can save the file even though it's an error. let's go inside of client.tsx and let's just check if there is somewhere here we should enable super json here it is so let's go inside of the rpc client let's go ahead and remove this part and let's enable transformer super json let's go ahead inside of server.tsx so trpc server and let's do the same thing let's check if there is somewhere here where we need super json looks like here we don't need it and let's do query client check here we need it so let's go ahead and uncomment serialized data and uncomment deserialized data and uncomment the import super json from super json then let's go back to client.tsx and let's add an import super json from super json let me just check that it's a named import it is so super json from super json should fix all errors and last but not least the init file i'm going to go ahead and import super json from super json so seemingly right now you won't notice any difference but i will try to capture a moment where it might be easy to understand why we need this again this is set and forget you will never modify this again. That's why I'm not paying too much attention to it. I'm not trying to disregard it as unimportant. I just don't want you to be intimidated if you don't immediately understand what it is. Okay, now that we have that ready, let's go back to what we attempted to build, the Voices router. So regarding the Voices router, the last thing we did is we connected it here, right? In the app router, make sure you have voices, voices router. And now let's go back here. And now we can extract organization procedure from the init file. Perfect. And what that allows us to do is it allows us to, well, use the organization procedure for various procedures that we need to do. For example, the first procedure I want to do is get all. That's going to be an organization level procedure. Let's define the queries for get all. So we're going to use zod and we're going to define params. It will accept query. Query will need to be trimmed and optional. And the entire parameters are optional. So if you don't want to pass query, you don't have to pass an empty object. And then let's go ahead and define this query. So an asynchronous function. Let's define, let's extract context and input from here. And now let's go ahead and use this optional query to check if we should enable a search filter across our Prisma database query. So if we have input question mark dot query, let's go ahead and do one thing otherwise let's go ahead and do another thing so in the important one let's go ahead and write a prisma query by adding or and then two options so if we have a query meaning if we have a search let's go ahead and search by name in first element so contains input dot query mode insensitive as constant. So we are using typecasting here because you've probably noticed that we are writing Prisma filter here, but we didn't really define a type for it. So you can see this doesn't throw an error, but it should. So we are using as const to stop it from erroring when we eventually pass it down to a Prisma query. That's why we're using typecasting here. All right, so yeah, you should be careful with your typing here. Or name contains input.query mode insensitive as constant. And in the second, we're going to query in description. So either we will try to find users search in the voice name or in the voice description. So again, if it contains input query and mode insensitive. So this way we are searching through voices titles or should I say names and their descriptions. Great. And yes, the else can be empty, just an empty object. and then in here we're going to go ahead and load separately custom voices and system voices. We can do that by calling Prisma voice find many within a promise all. So since it expects two results we need to have two Prisma voice many. So I'm going to go ahead and duplicate them and in the first Prisma voice menu, I'm going to go ahead and open a where and in this where, I'm going to go ahead and search for a specific variant of custom and only custom voices whose organization matches the currently logged in user's organization ID. What is context.organization ID? How did I get that? Well, let's take a look. Where does the context come from? It comes here from dot query. How is this populated with user ID and organization ID? Because of the organization procedure. If I change this to base procedure, you can see down here we have an error. Organization ID isn't guaranteed here, right? That's why we invented the organization procedure. It extends the base procedure and safely appends user ID and organization ID. That's why we needed to do that. Okay, so we are fetching all custom voices made by this organization and we're simply going to spread search filter. If you wrote anything incorrectly within the search filter, you will get errors here. I think we can try it out if I do something like this. Maybe not. Okay, not my proudest code but yeah it's a way to like not uh do the same search filter advice perhaps it's safer to do it because this isn't throwing me any errors so we'll see uh and what we have to do is we have to give it an order by so after where let's do order by created ad so the latest ones and let's use explicit select here to only choose what we want to send to the front end so i want to send the id name description category language and variant so the reason i'm using select here is because i don't want to send r2 object key for example now technically you could use omit and then just do r2 object key true you could do that but i think it depends on your like safety threshold because imagine if alongside R2 object ID you add another super important super private field here you will have to remember to update all of your omit fields right so select is more explicit than omit so it's a matter of you know your privacy security threshold and So make sure you just have these. And then in this one, we're going to go ahead and have a similar where, but it's going to query by a system, right? In here, we are querying by custom, which requires adding an organization ID. But in here, we're just querying all built-in voices. Let's go ahead and add an order by here. And let's add the exact same select. Now let me go ahead and see what did I do wrong here. Await promise all which was supposed to be an array. My bad. So open an array within promise all. You can see the square bracket which I've just added and end it here. And now there should be no errors. Perhaps I can retry this now. If I change this. Okay, still nothing is telling me that it's wrong. So yeah, okay, I'm just going to leave it like this and we're simply going to see if search works or doesn't. Excellent. So now you can see we have custom and we have system. So we are separating them on the backend. So we don't have to do any filtering on the front end and we can just return them like this. Custom and system. So if you go ahead and do npxprisma.studio, and you will see that all of the voices we have added in the previous chapter have a, where is it? Variant of system. So these are all system voices. So currently the custom is always going to be empty because we don't have functionality to create new voices yet. But that's what we're doing. We're separating them on the backend. So we don't have to do a massive slow filtering on the front end just for an improved user experience. Now let's go ahead and add another procedure here. So this is going to be a delete procedure. Again, we're going to use organization procedure because only users from an organization should be able to remove something. The input it will accept is an ID. And this is not going to be a query. This is going to be a mutation. Okay. So what do we do inside of the mutation? Well, first things first, we need to find this voice using the ID. We need to check if it exists. And more importantly, we need to check that this ID that was just passed has a variant of custom because we don want to allow a user to delete a system voice So only if the ID that the user attempts to delete truly exists in the database exists with a variant of custom, and exists with that ID, variant of custom, and a matching organization ID of the currently logged in user, only then shall we proceed forward. And immediately, if that is not the case, we're going to go ahead and throw an error. Technically, we could have called Prisma delete directly here, but I like to have explicit errors such as voice not found. Maybe there's even a way to do that. I don't know. But now we can safely do this, which is just a simpler delete query. And what we should do now is we should also clean up our R2 storage. So if we do have a voice R2 object key, Let's go ahead and await delete audio voice R2 object key and simply catch any errors here. So obviously, this is something that could be improved in a way that we're not really doing anything if this fails. So there is a chance, you know, if this scales, hopefully, I hope you manage to scale this to millions of users. there are probably chances you will have some orphaned R2 uploads because eventually this will start to fail right because one in a million chances start to increase when you have millions of users of course so because of that what I would recommend doing here in the future you know when you go through production is try to put this in some kind of background job with automatic retries or something like that or at least have some cron job scanning for orphaned R2 uploads so you don't have something filling your storage that's actually deleted from the database. But for now, for tutorial purposes, this is more than okay. And once that is deleted, let's go ahead and return. So if you want, you can add a comment. In production, consider background jobs, retries, cron jobs, etc. Brilliant. So we now added two procedures. one to get all voices and one to delete a custom voice. Great. Now, I want to go ahead and build the UI for this. So I want to be able to fetch the voices and display them in a voice selector. In order to do that, we need to install at DiceBearCollection and we need to install at DiceBearCore. These are two packages used for a very simple task of creating unique avatars, which is just a very nice way of displaying our voices because otherwise it just doesn't look as good. Now I'm going to go ahead and go inside of source components and I'm going to create a folder called voice avatar. In here, I'm going to create use voice avatar.ts and I'm going to go ahead and add the whole thing. So we need use memo from react, create avatar from dice bear core and glass from dice bear collection. So if you search for Dice Bear, you will find a bunch of collections you can use. Be mindful of their license, of course, and you can customize these avatars as you wish. I find GlassOne to be perfectly fine for this project. Now that we have this reusable hook to generate the voice avatar, let's create the actual component. voice avatar dot dsx let's mark it as use client let's go ahead and import the existing avatar components from chat cn which are avatar avatar fallback and avatar image then let's go ahead and import our cn util which helps us with class names and let's import our recently created use voice avatar from dot slash use voice avatar let's define the props which are required string my apologies recall required seed required name and optional class name then let's go ahead and export function voice avatar which has seed name seed name and class name props as we defined above and in here the first thing we want to do is we want to generate the avatar using our use voice avatar and passing in the seed and this will return us avatar url and fun fact this will generate an actual svg it will not actually create a network request to some url right it's just that this is technically a url i think it's just base 64 generated url okay let's go ahead and return and let's build this simple avatar component so the avatar component is going to have a class name size 4 border white and shadow extra small let's go ahead and fix the typo and let's make this dynamic and just passing class name which is a prop. So if the user wants to modify this beyond what it currently is they can do it using the class name prop. Let's go ahead and render the avatar url and give it an alt of name to the avatar image component and let's create a simple avatar fallback with a very small text in case for some reason avatar url fails this will almost never happen because we're using base 64 instead of a network request so i don't see how it can fail but still it's nice to have a fallback which will simply you know display two letters Great. Okay, that's it for the voice avatar. Now, what we have to do is we have to build the text-to-speech voices context. We need this because it's how we're going to populate the voice selector. So let's go ahead inside of features, text-to-speech. Let's go ahead and open a new folder, contexts. and inside of contexts let's go ahead and do text to speech voices context dot tsx and in here i'm going to add use client we're going to import create context and use context from react we're going to import type infer route outputs from trpc server again from the package not from our at forward slash, okay, from the package. And we're also going to import our app router type from our trpc router's app. So what app router is, it's basically a single type definition of our entire API. You can see that in here we have voices and the voice router, and here we have the export for this. And in here you can see I have voices. I have get all procedure. I see exactly what input it takes and exactly what output it provides. And now we have to define the type of a singular text-to-speech voice item, which is basically a single voice which has ID, name, description, category, language, and variant. Since custom and system share the exact same filters, we can choose which one of those we want to use. So I'm going to go ahead and write the following infer router outputs, give it app router, and then simply traverse through the router. So we are going into the voices router, get all procedure, and you can choose between custom or system. They are the same. And simply use number. This indicates any item in the array. And now when you hover over TTS voice item, you should have the exact same item here. Now let's go ahead and create the interface for this context. So text to speech voices context value will have custom voices, system voices, and all voices. all sharing the same prop, an array of TTS voice item. Then let's go ahead and create the actual context using createContext, passing the value from above or null if it's the initial value. Now that we have that, we can go ahead and export function TTS voices provider. This provider is going to have children and value. The value is a type of TTS Voices Context value. And very simply, let's return the provider. TTS Voices Context, which we defined above, dot provider with a value, value from the props. And render the children inside. As simple as that. And then let's go ahead and export one more thing. Use Text to Speech Voices function. this hook will be used to access, well, custom voices, system voices, and all voices in any single component, which is wrapped inside, which is basically rendered inside of the children. So let's go ahead and get the context using useContext, passing text-to-speech voices context. And in here, let's throw an error if the context doesn't exist. So if a component attempts to use this hook outside of this provider, we have to throw an error. This is a standard practice when developing context. You've probably seen this error yourself from various other packages. For example, if we attempted to call use trpc without this, it will throw the same error. That's how context works. So this is the equivalent of us building somewhat of how TRPC built their context and provider Much simpler of course but I think you get what I trying to say here All right. And now we can build the actual voice selector. So I'm going to go inside of source here. I'm going to go inside of features, text-to-speech components, and let's open voice selector.tsx. let's go ahead and import use client and use store from 10 stack react form let's go ahead and import voice category labels from features voices data voice categories so if you remember it's basically a remapping of voice category and new from prisma schema into user-friendly labels fixing capitalization, adding spaces. I chose to do it in a manual way rather than programmatically. Technically it could be done but I feel just better doing it this way. And then let's go ahead and import the following field and field label from components UI field. Then every single thing from select which is select content group item label separator trigger and select the value. Then let's go ahead and reuse our use app form. So basically it allows us to share our form values. If you remember, in this component, in this component, and now in this component. That's why we need it because there are three separated components who share the same form. So that's why we needed to develop it in this way. right we created a context for that here then let's go ahead and import our newly created voice avatar then let's go ahead and import use text to speech voices from our newly created context folder text to speech voices context so outside of components in here inside of text to text-to-speech voices context. And besides that, we also need text-to-speech form options, which we have in the same folder. So where it is text-to-speech form, it's right here. So we are reusing this. So we have the exact same form access in a completely separate component. Perfect. So let's export this component called voice selector. And immediately, we can go ahead and get custom voices, system voices, and all voices using the use text to speech voices hook. The only thing we're going to do is we're going to remap all voices to voices for simplicity's sake. Then I'm going to go ahead and define the form. Use typed app form context and pass along text to speech form options. Then what I'm going to do is I will check if I have a voice ID inside of my default values from the form. Because I have to somehow keep track of what is the currently selected voice ID. so I'm gonna do that with use store selector hook so reading from form.store I'm gonna read s.values.voiceid and that will give me voice id and this is completely type safe right because voice id exists inside of text to speech form options that's why we are able to read it and this is how we will get the current value of voice ID. And in the same way, let's add is submitting. Then let's go ahead and see, is this voice ID, which we currently have in our form, actually existent? What does that mean? Well, something in our form currently has a value, of voice ID 123. But what we have to check is does this voice ID exist within custom voices or system voices or even all voices? Because if it doesn't, it can either mean it's invalid or it existed before but now it's deleted. So because of that, we need to carefully search for it. right? Selected voice. Let's go through the alias for all voices, which is voices. And let's search for a single voice with a matching ID. Okay. And now let's check if it has a missing voice. How do we do that? Well, let's define has missing selected voice if either voice ID is missing or if we cannot find a selected voice. And then let's go ahead and decide what to do. Okay, so what's the current voice? Well, it will depend on the following. If we have selected voice, let's go ahead and use the selected voice. As simple as that. Otherwise, if we deducted using these two rules that this is a missing selected voice, we're going to go ahead and add a ternary here. And immediately we can fix the other part. So this is if and this is else. The else is simply going to say, okay, just display the first item in the array. That's it. Okay. But otherwise, what we're going to do is we're going to give this a name of unavailable voice. So if a glitch happens, we are going to display to the user like, I don't know how you got this voice, but it is unavailable. I cannot find it in an array of items here. Okay, that's what we're doing now. okay and now we can go ahead and build the actual ui so in here we're going to go ahead and do some form composition here using field label to call this voice style then we're going to render the select component give it a value of voice id on the value change of value calling form set field value and control the property voice id and populate it with the new value disabled if the form is submitting then we're gonna add a select trigger component with a class name full width height auto gap one rounded large background color white px2 and py1 then we're going to render the select value in the select value we're going to check if we have the current voice okay and if we have the current voice we're going to open a fragment and inside of the fragment we're going to add two elements. The first element is going to be a voice avatar which will accept the current voice id and the current voice name to generate a unique looking avatar and the second element will be a span with a class name of truncate, text small, font medium and tracking tight. In here, we're going to go ahead and render the voice name and then beneath, or should I say next to it, we're going to render a current voice dot category. But we have to check if the category actually exists. If it does, we're going to go ahead and add a space, another space, sorry, space, dash, another space. And then in here, we're going to go through our voice category labels and pick current voice dot category to map to a human readable result. There we go. Perfect. And that's it for the select trigger. Now let's go ahead and let's build the select content. So that's outside right here. And let's check if we have a missing selected voice, but we have managed to populate the current voice. in that case let's go ahead and do the following here so we're going to select group then we're going to add select label with selected voice we're going to add selected item with value current voice id we're going to render the voice avatar with seed and name we are going to repeat the span from above, right? Current voice name, current voice category. If it exists, it's going to be mapped to human readable voices. And the class name will be truncate, text small, and font medium. And then we're going to go ahead outside of the select group here. And we're going to check the following. So let me try and expand a bit. So if inside of parentheses, custom voices dot length is larger than zero, and system voices dot length is larger than zero, and only then render the select separator. Okay. And now we have to render the other part. So outside of this fragment, and outside of this curly bracket and this normal bracket. So basically what this was, This was a category for a missing custom voice. But now we're actually doing the real custom voices here. So let's go ahead and render a select group. Let's render a select label with Team Voices text inside. And then let's iterate over custom voices.map. Let me see what's wrong here. okay I have to render something custom voices dot map will simply use select item and okay no that that's not what it was let me check something is wrong here am I missing I am missing another parenthesis there we go select item has a key and a value and inside we're going to render voice avatar with seed and name. And then we're going to go ahead and repeat the span element once again. But this time it can be simpler because at this point, it's guaranteed that a category label exists. Okay. Because this isn't some scenario of a missing voice right Which God knows what category it had at the time Perhaps we decided to deprecate that voice Maybe it was deleted from the database We can't really know. But this one is a simpler use case because we know this is currently active, this exists. There's no reason for us to do the complicated label thing. And then outside of this select group, let's go ahead and let me see. My apologies, not here, not outside of select group, outside of this. here. Let's check again if custom voices at length is larger than zero and system voices at length is larger than zero. Let's render a select separator and now in here we can go ahead and we can actually copy the entire thing here and paste it and simply replace custom voices with system voices. And we can replace the label which was team voices and change it to built-in voices. and everything else is exactly the same, right? They have the same props. They look the same. They have the same class name. That's it. That is our voice selector field. So it looks complicated, but it isn't really. It's mostly complicated because of this weird custom voice has missing selected voice, but we need to cover that case because we allow voice deletion, all right? So because of that, we need to think of user experience. another reason this might happen is later if you have a billion voices you might decide to add pagination at that point you should still kind of you should just say unavailable voice until you adjust this to work with that pagination right you're gonna have to rethink a bit how it works but i mean this isn't really expensive to load because these are just like strings so now that have this and we built the API endpoint let's go ahead and let's render this properly so we're going to go inside of source app folder dashboard text-to-speech page and in here we have text-to-speech view and we're going to adapt this a little bit by adding trpc hydrate client and prefetch from add trpc server this time from our trpc server right so if you do this it doesn't work if you add a forward slash it works it's basically our server instance with hydrate client and prefetch function so add all of these and then what we're going to do here is we're going to go ahead and add search params as a prop because remember we can now do searching and let's go ahead and do this search params and in here we will accept text and voice id so okay i just told you it's because we will accept searching. My apologies, that's not what these search params are for. What these search params are for, are for this scenario. So you remember the homepage. On the homepage, when I click try now, look at the URL. The URL has text. So that's how we are going to populate the form, by looking at what text was passed. And we will also later be able to pass a voice ID through the URL. That's what this is for. My apologies. I confused this with our search functionality, which doesn't come until later. So let's go ahead and extract that using await search params, which means we have to turn this into an asynchronous function. and once we have text and voice ID here, we are ready to prefetch. So let's prefetch trpc voices get all and simply empty query options. And then, instead of just rendering text to speech view, we're going to render hydrate client wrapping the text to speech view. Okay. And let's go ahead and do one more thing. Let's extend text to speech view with initial values, passing along the text and the voice ID. All right. Now that we have added that, let's go ahead and modify the text to speech view component. So inside of features, text to speech views, text to speech view, let's start by modifying the props. And for that, we're going to need to import, where is our text-to-speech form? Here it is. Let's extend this import by importing type text-to-speech form values. And then inside of this function, we can go ahead and request initial values. And let's go ahead and give it the proper type. So they are optional, but their proper type is a partial version of text-to-speech form values. So all of these could technically be passed through URL, okay? But we will for now only allow text and voice ID. We don't want users to, you know, do features which we didn't enable ourselves. Okay, great. And now what we have to do, we have to populate our text-to-speech context with voices. We can now do that because inside of this component page, we just prefetched all voices, meaning we now in this cache, which we're going to call here, have access to all the voices. So let's go ahead and add the following import. I'm going to import use suspense query from tan stack react query and I'm going to import use trpc from at forward slash trpc client. Again this isn't a package this is our trpc client. Great and we also need to import our newly created context. So text-to-speech voices provider, context, text-to-speech voices context. All right. Now that we have that, we can initialize trpc with a hook. We can go ahead and fetch the voices. So let me go ahead and collapse this so you can see it in a more readable way. use suspense query, trpc, voices router, get all procedure, empty query options. The exact same one we prefetched here. So this is essentially using the same key. So it's going to read from cache and it will say, hey, I already have this because the server component has prefetched it and hydrated edited to me. So it's a much faster load than it would be if we just did use query. You can do that, but it makes no sense since we have access to server components, right? Let's leverage them because we chose Next.js, so let's use Next.js, right? And from here, we have access to data, which are voices, custom and system, right? And now we can, just for readability's sake, destructure them like this. So from voices, we can extract custom and system. Let's map this to custom voices and this to system voices. And then let's go ahead and generate all voices. How? Well, by combining them together. and then let's go ahead and define the fallback voice id to be the first item inside of all voices or if even that doesn't exist fall back to an empty id all right now let's go ahead and do a scenario where a voice no longer exists if it's deleted for example so requested voice may no longer exist deleted. So let's fall back to the first available voice. So I'm going to go ahead and do resolved voice ID. We're going to look through initial values and check if it has an existing voice ID. Let's check if in those initial values we can find a voice ID. Okay. If we can, we are simply going to allow the user to populate this form using the initial values voice ID. But if not, we're going to have to fall back to another. Okay, what does that mean? Well, if someone in the future calls text to speech with voice ID deleted, right? This voice ID no longer exists. We cannot just blindly populate that to our form, all right? Because we have access to all voices here. So how about we check against them to see if that voice actually exists, right? So if they add a proper ID and we find it in all voices array, which is a combination of all custom voices and all system voices, sure, proceed. Otherwise, fall back. That's what we solved with this, okay? I know it's a bit confusing to do it now. I say now because we don't really use it now, but I will forget it if I don't implement it now, okay? In the default values now, which we cast as text-to-speech form values. Let's spread default text-to-speech values. Let's spread default initial values and our custom solution for the voice ID. So we get rid of a deleted voice ahead of time. Great. And now we have to wrap the entire app within a text-to-speech voices provider. So let's go ahead and do that. There we go. And in here, we have to pass the value. This value will be custom voices, system voices, and all voices. There we go. So now, every single component within text-to-speech form has access to all voices at all times. and we also have to modify the default values of text-to-speech form with default values here.\nAnd you can already see something happen here. Let's go ahead and go back to dashboard. Try clicking on one of the try nows. You will see that this is automatically populated now. So that's what we just did. Okay. We enabled initial values passed through a URL. But text, this text is the easy part. The hard part was the voices. Why do I say it's the hard part? Well, it's easy to just load an ID, but the problem is what if we pass a voice ID through the URL, but we didn't load the voices yet? That's a problem. We can't know if it's been deleted. We don't know what avatar to assign to it. We don't know what category to assign to it. We don't know anything besides its ID. That's why this complication exists. okay we are doing this in a industry standard way so before the form initial values are initialized we first fetch the record that the user is trying to out of fill uh the user is trying to initialize right give an initial value to that's why we're doing this complication i hope this kind of clears it up great so i believe that's it for the this component i think we might be at the last component now that is components settings panel settings okay so this component right here can finally now render the voice selector so let's go ahead and remove the paragraph we can leave the id we can leave the class name as is and let's simply render the voice selector it's a simple import dot forward slash voice selector because they are in the same folder if you want to you can change it to components like this and you can see that we didn't have to pass any props to it because it uses use text-to-speech voices it has access to these voices right here okay it also has access to the populated voice id and because these voices are already loaded, it is able to check if this is a missing voice or not. So that's what this complication was all for. Okay. I hope I've kind of cleared it up now. And here it is. So let's try it out if it works. What I say if it works is this. Let's open Prisma Studio. Find the voice that you want. I don't know. Emanuel, I'm going to choose that. Copy the ID of the voice. and I will go ahead and do the following. Go to text to speech and add forward, sorry, add question mark voice ID and add it here. So text to speech, voice ID, and then paste the ID. Okay, let's see. And manual is selected. Let's go ahead and see what happens for a random one. you can see it falls back to Aaron, to the first one in array. Okay, that's what the complication was for. But yes, we now have a fully working voice selector. Looks great. The only thing we cannot see right now is the category for custom voices because we don't have any just yet. Great, so we learned a lot here. We created the TRPC Voices Router. We created a context for voices. We work with DiceBear avatars. We even did prefetching once again. The only thing left to do now, which isn't really related to this voice style, but let's go ahead and do it. We're going to learn how to create the Chatterbox client with open API types and fetch. All right. After some consideration, I actually decided we are not going to do a text-to-speech client in this chapter for a very simple reason that it has absolutely nothing to do with voice selection. So I think it's better that you pause here and understand what we just built, right? And so that we have a pull request which matches the voice selection because what we plan to do next is start doing text-to-speech generations. So it makes more sense to generate the open API typed client there rather than here. So no third step. We are finished at this point. I think this is a natural step to end for this chapter. And I think most of you will agree. It makes no sense to learn how to self-host Chatterbox after doing voice selection, right? All right, meaning we are done. So let's go ahead and quickly go over these files. So I have 16 changes. One of them is package lock and package JSON. I have some deletions of health check and test page. But other than that, I seem to be having these modifications for TRPC. These are mostly enabling super JSON and adding organization procedure, out procedure and in the app folder we added the voices router everything else are components we need so you should have those the same as me great so this is chapter five so let's go ahead And let's do git add, git commit, 05 voice selection and git checkout b, let's say 05 voice selection. All right, git push u origin 05 voice selection. you can see we checked out on that branch and now let's go ahead to our source code let's open up pull request and let's create a pull request and let's wait for the deployment and let's wait for the review one thing i forgot to do npm run lint and npm run build but i'm fairly certain both will work. Lint has no problems except those warnings which we had from the first chapter. Looking good. All right so let's wait for the review. And here we have the finished CI CD. So down here I can see my railway app is deployed. Let's go ahead and check the web instance here. There we go. Seems to work. Text to speech and here we have our voice selector with all of the voices we have seeded. Amazing. Let's check if the pre-fill works. Pre-fill works perfectly fine. Great. Our app works in production. Now let's go ahead and take a look at the summary by CodeRabbit. Voice avatars now display in voice selection UI for better visual identification. Text to speech page supports URL parameters to preload text and voice preferences. enhanced voice selector with improved organization and categorization and voice management functionality for viewing and deleting voices, deleting currently only on the API side. So let's take a look at the three comments we got left here. The first one I never heard of. I did not. I mean, I probably heard of it, but I wasn't aware of it for this specific scenario. normalize search params values before passing them into form defaults. According to the Next.js app router specification, repeated query key produce array values. So if someone redirects to our page with multiple text values, that will result to text being an array, which will break all logic that we have around this. So we definitely have to do something about this. I mean, our app has no way of doing that. So someone would have to manually do this to break our app. At that point, the user is at fault, right? But we could do something about it. Definitely a good comment by CodeRabbit here. I did not think about normalizing the search params. I will see if this is something that we have to do or not, because you can see they labeled it as a minor as well, simply because this is something that we will not do. So only users might do this, or we can do it by accident, right? But I think they're mostly telling us, you should use the proper type so you can understand what might break. That's what they're telling us here, to change it to string or an array of strings. So great, great comment by CodeRabbit here. In here, it's technically telling us that we need to add an explicit empty state guard in case we forget to seed system voices. In our case, the entire app will not work if we don't have system voices. So not really point in adding more guards than we did. And we did a lot of guards for an invalid or missing voice ID. We'll just display an empty voice ID then, right? And in here, it's telling us what I told you, right? We are currently swallowing up failure, but obviously in production, you should retry or at least consider putting this in a background job. Other than that, we did a very, very good job here. So let's merge these changes and let's go ahead and get checkout back to main and get pool origin main. There we go. That should synchronize the entire thing. And I'm going to go ahead and check that we are on main here we are and let's go ahead and look at the graph there we go 05 voice selection merged to the main branch I believe that marks the end of this chapter so yes I decided that we're not going to do a text-to-speech client in this chapter simply because it makes no sense to do that because in the next chapter we're going to do that and the actual generation trpc procedure and i think it fits better there so we understand why we build uh why we do that right because if we do it now we're just gonna forget why we did it by the time we come to the next chapter so amazing job and see you in the next chapter in this chapter we're going to wire up the entire text pipeline we going to learn how to self chatterbox text using FastAPI and Python on modal serverless GPU This will provide us with a typed API client that we can then use in our trpc procedures in a fully type-safe way. We're also going to learn how to create audio using that Chatterbox server and automatically store it into our Cloudflare R2 storage. Then once we have that wired up, we're going to focus on building the UI. We're going to use wavesurfer.js to create cool waveform visualizations of the audio generated, as well as seek controls and download functionality. So by the end, you will be able to type a text, Pick a voice, hit generate speech, and it will play back the generated speech using the voice you selected with cool waveform visualization and all of these other controls. So the key concepts are going to be self-hosting Chatterbox text-to-speech, trpc mutations, using FastAPI and Python to create open API type generation, signed URL proxies to listen to generated audio, storing audio in Cloudflare R2, and using a cool package called Wavesurfer.js to create amazing waveform visualization. So the build order is going to be first self-hosting Chatterbox text-to-speech and generating typed API. Then building the text-to-speech TRPC generation procedure. And finally, Wavesurfer player for waveform and controls. So the first step is learning how to self-host an open source model. So first, let's discuss what model we are self-hosting. We are going to be self-hosting Chatterbox text-to-speech. Chatterbox is a family of three state-of-the-art open-source text-to-speech models by Resemble AI. And more specifically, we're going to be using Chatterbox Turbo, their most efficient model yet. So where are we going to self-host that? We're going to self-host it on Model. Model is an AI infrastructure that allows us to self-host, train, and do many, many other cool things with open weights or open source models. So let's go ahead and create an account on Model. And once you create an account, you will see a screen like this. So the first thing I want to clear out is the pricing. So at the top, you will see your credits remaining. At the time of me, making this tutorial, Model actually offers $30 every month for your free account. That's right. But right now it says $5. That is because you have to add a credit card to unlock the rest. So if you want to, I would recommend adding a credit card. You can see that they will only charge a small one-time fee and then refund it back to you. And then you will unlock additional $25 and it will renew every month. So a pretty good deal if you ask me, but if for whatever reason you cannot do that, you should still have $5 available. What we have to do next is we have to install model on our machine. So before you run any of these commands, I'm going to go ahead and show you what that looks like when it's finished. So I have an ability to run model dash dash version. Now, I'm not a Python developer, and I'm not experienced with Python tooling. So for example, you can see on my machine, I don't have peep. I don't even have Python. but I do have Python 3 and I do have PIP 3. Now, as I said, I'm not a Python developer so I'm not sure if you can just use PIP 3 install model and PIP install model, is that the same thing? But one thing that I found that made it so much easier for me as a JavaScript developer to install model is this package called PIP X. And it's purposely named like this because if you scroll down a bit to what is pipX, it's basically a tool that helps you install and run end-user applications written in Python. It's roughly similar to macOS's brew and JavaScript's npx, which is exactly the experience I want. So the way I installed model was using pipX install model. and the way you can install pipx is by following their guide. So the package name is pipx and in here you can see how to install it on macOS, for example, using brew, running pipx and surepath and then sudo pipx and surepath global. Then on Ubuntu, Fedora and Arch Linux and also on Windows, of course, using scoop. So whatever method you prefer, if you know Python tooling and you know how to do this, feel free to do that, right? So just make sure that you have model installed in the end. You can see the version I am on. Again, I don't think you need to be on the same version as me. I just want to make you aware what the version was at the time of me making this tutorial. And if you do it using pipx, I think you can also run a pipx list. And then in here, you will see all the packages installed using pipx. All right. Once you have model, let's go ahead and run model setup. And in here, it will basically ask you to create a token. And you will select your workspace and you will click authorize. and then in here you are now logged in into that new account that you have just created okay so if you want to go ahead and change your account if you want to log out you can just do model setup again now as i said if you did this using pip you're gonna have to do python 3-m model setup right but if you did it using pipx you can just run model from now on i think that's easier but again i'm not experienced with python tooling so uh be careful okay i mean be careful um research what works try what works for you that's what i meant to say we're not doing anything dangerous here so what should we do now well let's see if this works i'm gonna go ahead and copy this small snippet which computes the square of 42 okay using serverless functions so what i'm going to do is inside of my project here in the root of my app i'm just going to go ahead and run this get started dash by so get underscore started dot by and i'm going to paste the script in here So import model app model.app example get started app function. And in here we have a simple function to do squares. Okay. So we don't have to do this part because we just did it using an editor. And now what we should do is try and run it. So let's try and run model run get started.py. And you can see that this is now building an image and running an app. and here it is the result. And now in here, you can see that this has changed because we successfully did this. And inside of your stopped apps, you will see that example right here. All right. So that was a super simple example. So what should we do next? I recommend visiting their documentation page and in here, click on the guide. Okay. So in here, they actually have a more advanced guide. For example, this one teaches you how to self-host QAN, an open source model. It's more specifically an LLM inference. The one that we are going to do is right here under examples where you can find a bunch of cool examples of what you can self-host on model and you can select audio here and you can see everything that they actually offer you. You can make music, you can fine tune whisper. What we are interested in is deploying a text-to-speech API with Chatterbox. Okay, so you actually don't need to worry if you don't find this documentation page because I will provide you with the final Python code for running Chatterbox text-to-speech API simply because we will have to modify it from what it currently exists. The reason I'm showing you this documentation page is so that you can learn how this works. Okay, what are they doing differently here from what we will be doing? So let me try and collapse this. So one very important thing to understand, they are using their own storage here called modal.volume. They're using that for simplicity's sake because they have the functionality to do that. And this is actually where you have the download button for all of those voices, which I told you to add to your repository, right? So inside of scripts, we have system voices. Keep in mind that this file does have some other folders. Basically, you just have to find the audio files and just add them here. You don't need the other things that are inside the folder. And they basically upload all of those voices to their volume, okay? And then in their Python script, they connect to that volume. So we are not going to be doing that for a very simple reason that we need our voices to be able to be loaded in our Next.js app. So because of that I figured we should either use AWS or R2 basically some reliable S3 storage So what we going to do is we going to take this script that they created right here but instead of connecting to their volume you can see right here we are going to be connecting to R2 storage. All right. And then we're just going to go ahead and test if this works or not. So how do we test this? Well, here's the thing. I'm not a Python developer, so I don't feel too comfortable teaching you Python. So instead, what I'm going to do is I'm going to provide you with the source code of the final script, and I will go ahead and explain the changes I have made from their script so you can understand the code that you are running. so in the root of your app you will have chatterbox underscore text to speech dot python okay and you can remove the get started one and you can also remove the python cache folder if you want to you can just add it to git ignore and then it should be omitted all right and inside of chatterbox underscore text to speech dot python, this is where you would usually add their example, right? The documentation, examples, audio, text to speech, and in here you would just copy the entire script. But we are going to do something different. You can go to my source code, link is on the screen, and go ahead and find in the root of my app, chatterbox text to speech dot python. So this is essentially a more advanced version of what they are offering in their documentation. It is optimized for the following things. The first thing you immediately notice here is we are using R2 cloud bucket mount. So in here, we should add our R2 bucket name. And in here, we should add our R2 account ID. and you also have to verify you remember this we already had this a couple of times in our code right if i search through my code you can see i have three instances of that one in the seed script and one in the r2 lib so just confirm that you are not using the .eu one if you are no problem you will just have to modify the script to be that as well okay and you can also see that we have to add some secrets here. Now, what is the difference between this secret and dot environment file? Well, this script right here will be running on modal serverless functions. So in order for this file to access secret variables, we need to upload those secret variables to modal. Okay. and in fact they even have that process in this basic documentation right here so you can see they teach you how to create volumes which we don't need because we have our volumes uploaded to r2 storage right uh but later here somewhere they should be teaching you about uh hugging face uh where is it model secret yes you need to provide a hugging face token using model dot secret. I'm just not sure where exactly is the script for that. But never mind, I know what the script is. All right. So what do we have to do next here? Well, let's do the easy thing. Let's go ahead and rename the bucket name in the account ID because we already know the information about that. So I'm going to go inside of dot environment file here. And I'm going to copy the account ID. and I'm going to add it here. We're going to go ahead and copy the bucket name and I'm going to change the bucket name here. So ResonanceApp, account ID. And in here, we simply now mount to that R2 bucket. So this bucket with this ID and the secret which we are going to upload to model. So I think this is this. I'm pretty sure this is the secret access key that it needs. We're going to upload that later. Then in here, we go ahead and we set up a very slim Debian image. And in here, we define a couple of packages. Chatterbox text-to-speech, fast API, and this package, I'm not sure what it is. We go ahead and we do the normal instance of model app as they do in their example right here. But what we continue to do forward is we protect this Python server with something called xAPI key. Basically, once we deploy this to model, I want to make sure no one can access this code, can access this server, except those who have Chatterbox API key. So we are protecting API in general, every single endpoint with that API key. And the only app that's going to have it will be our Next.js app. So our Next.js application will be the only one who can communicate with this model serverless function. Another thing that we do, which is not shown in this documentation, is we enable all of these prompts. I mean, all of these params. So besides the prompt, which is basically the text users want to generate audio for, we also add voice key, temperature, top P, top K, repetition penalty, and loudness. And if those sound familiar, it's because we have them inside of our features text-to-speech data sliders. Here they are. Let me go ahead and find them. temperature, top P, top K, repetition penalty. So those are the properties which we allow our users to modify, right? Those translate to creativity, voice variety, expression range. That's what those fields are. They are not exposed in their basic example here, but by reading the Shatterbox text-to-speech source code, you can find that they do offer those fields. So what do we do then? Well, for example, this part is assigning a GPU instance to this serverless function. I'm not exactly sure what this represents. It's just a name of the GPU that we are using, and it's the exact same that they are showing in their example. So it's the one that's supported by free tier. They, of course, do have more powerful ones. If you are more experienced with this, you will probably know that, right? And this is a scaled down window. This basically means how long until this server function goes cold. Once it goes cold, it means it will take some time to start up during the initialization. Okay, you can completely eliminate cold starts, but you will have to upgrade to a higher tier on model. So for free tier, you will simply have to live with cold starts, which I think is a fair deal given the amazing service they are providing. So now we have the secrets. They also have secrets, but they only use one. They use Hugging Face Token. Hugging Face Token is free to obtain and it's needed for Chatterbox. Okay, so we have that as well. But we extend it with also requiring Chatterbox API key, which is basically the protection so that only our Next.js app can communicate with this serverless function and Cloudflare R2 secret so that this Python server can connect to our Cloudflare R2 storage where we actually have the system voices. All right. Then we define Chatterbox Turbo Text Speech from pre-trained device CUDA. So exactly the same as they do. So you can see this is obviously modified from their instance, right? And then in here, we define the OpenAPI Swagger documentation. So we set up course middleware. So what I do here is I allow everyone to connect here. It works for me. I mean, it works for us for a tutorial. Later, you could probably only change this for your deployed URL, but that's kind of already protected because of the chatterbox API key here. Then we define some endpoints. We create a post endpoint for forward slash generate. And in here, we simply attempt to find the voice key inside of our R2 cloud storage that the user has selected. So if we cannot find the voice user has selected within the R2 cloud storage, we raise an exception, voice not found. Otherwise, we go ahead and we generate it using the prompt, the voice path, the temperature, top P, top K, repetition penalty, and normalized loudness. And we return back audio response. If anything fails for whatever reason, we throw a generic 500 fail to generate audio. And down here, we have a local entry point, which usually I'm not sure is a practice for fast API servers at all. Again, I'm not a Python developer. I'm not sure about how best practices and tooling is done here, but they have this example. And I think it's a cool example because it will allow us to test if this works using CLI. It's basically a copy of the code above, but not scoped to an API endpoint, but rather to a local entry point you can run with CLI. All right, so I wanted to go through the code. So because I told you to copy and paste it, so I wanted to show you that it's not some random thing I found. I just modified it with AI assistance and with my general knowledge of API endpoints and how, I mean, APIs and how they're built. But as I said, I'm not a Python developer. So definitely for serious production instances, I would recommend checking this code once again. but for tutorial purposes, it is more than enough. And it is generally very safe because only our Next.js app will be able to access it. No one else. If it doesn't have access to your API key, it is completely protected from any malicious actors. All right. So once we have that, we have to go ahead and add some secrets. So what secrets should we add? Well the first one that we have to add is HuggingFaceSecret To obtain the HuggingFace token we need to create a HuggingFace account So using the link on the screen you can go to HuggingFace and go ahead and create an account Make sure that after you create your account, you also verify your email. And since this is mostly an AI hosting inference website, don't be surprised with a huge amount of verify you are human prompts. They are normal. Okay. Once you're logged in, go ahead and find your profile at the top here. Okay. I just zoomed in too much. Find your profile and find access tokens right here. In here, click create a new token. I'm going to go ahead and simply call this Shatterbox Text to Speech and since I'm not really sure which ones we need I tend to select everything so let me go ahead and actually let me try and find the exact one that we need because I'm not sure if we should select all of these or only some of these so the permissions I have given it in my first iteration of building this app was right So go ahead and select write for the access token. So let's go ahead and click create token here. And let's go ahead and copy the value of this token. Now, if you want to just preserve it, you can add it to your .environment file, even though it's not exactly going to be used in this project. It will be used in model, right? But you can add hugging face access token, or you can just store it as hugging face access token like this. And you can store it here simply because this is not committed, so you don't lose it. And let's click done. There we go. So now we have that token. And now we should go ahead and learn how to add it. So you can see that in my project here, I do have something called secrets, but I don't have any secrets here. So let's go ahead and learn how to add a secret. So model secret create hf-token and then hf underscore token. And then in here, paste your token. Let's go ahead and see why we need to use this exact name. So it's not made up. It's exactly matches the content of Chatterbox text-to-speech Python. Okay, so let me try and find fh-token. here it is fh-token that's what we need f hf-token not fh all right and let's go ahead and add that to see if it works so created a new secret token hf token with the key hf token so you should now be able to um well i'm just searching for something here but i don't think i'm gonna find it never mind. Now, if you go here and refresh, you should see your secret. Here it is. And they even show you how to use it. And from here, I'm not sure if you can see the value of it or not. But basically, yes, you should now have access to this secret. Okay, that's the first one. Now, we need to add the second one. The second one is going to be model secret create Chatterbox API key, Chatterbox API key in capital. And in here, you simply write your super secret key. This should definitely be something generated. If you are going to make this into a real product, please don't forget to change this. I'm making something, I'm adding something simple now just for tutorial purposes so it doesn't get lost. Okay, so let's just go ahead and add this secret. And let's immediately go to our dot environment file here because this one actually will be used here and let's go ahead and add chatterbox underscore api underscore key and let's just copy super secret key value that's the one we're going to use and you can see now we have two secrets here okay and then there is one more that we need and that is the following so we are going to add model secret create cloudflare r2 okay let me try and do this without autocomplete sorry because it's it's interrupting me cloudflare dash r2 go ahead and add a backwards slash okay maybe it will be simpler for me to just because you have to do it all at once but I'm not too experienced with command line interfaces to tell you how do you go into the next line without submitting so what I'm going to do inside of chatterbox text-to-speech python I will add this okay and I'm going to comment it out and I will simply write use this to add r2 tokens okay so you you will be able to see this in uh the source code okay so go ahead and uncomment this copy it comment it again and then very simply paste it okay and now we're gonna go ahead and modify it so aws access key id uh should match r2 access key id and yes we are using AWS, even though we are working with R2, simply because the cloud bucket mount that model provides us with to connect to other instances reads from those variables. So they need to be called like this. Okay. So I'm going to go ahead and copy the secret key, secret access key, and I'm going to paste it here. And then I'm going to copy the account ID. and I'm going to paste that above. So basically, maybe you could add these separately, but when I tried, the moment I run Cloudflare R2 and submit a single one, I can no longer append it with another one. That's why you have to do it at once. In case you already submitted, no problem. You can always delete a key from here. right? So let's go ahead and add those two keys. Here they are. We now have AWS access key ID and AWS secret access key. And now we are ready to try this out. Perfect. So the first thing that we're actually going to do now, just make sure this is uncommented because otherwise it's going to go ahead and break your app. The first thing we're going to do is we're going to deploy the chatterbox so model chatterbox text-to-speech python my apologies model deploy chatterbox text-to-speech python and let's go ahead and see if this succeeds or not so for the first time it might take a while because it's building the entire image it's downloading all the packages it needs to download and it is uploading to model so let's give it a moment to do that and once it successfully deploys you will see your end point right here and here's a little tip so the first time you attempt to load this it will take a while so don't be surprised that is because of the cold start so this will definitely take a while just go ahead and leave it here but for now you should see your live app. Here it is, Shatterbox live app. So let's go ahead and wait for this to deploy, I mean to load, and then we will see exactly what we can do with this. So my container kept failing, and no matter how long I waited, the errors kept increasing, and I think I know why. I'm not sure if you will have the same thing happened. Maybe you will because I told you to try and do a GET request using your browser, but I forgot that we actually protect everything using the API key. So yours probably failed as well, but mine failed for a very specific reason. You can see that all of these failed at runtime initialization. And that told me that something was wrong here. So I went to look through the logs and I looked at the error message, which was written. And it looks like the AWS S3 is unable to connect, unauthorized. And then I simply, you can see I was doing some testing, like why is it failing? But then I went back to this. my modal secret create Cloudflare R2. And I noticed that my AWS access key ID is incorrect. I have added the wrong one. I added my account ID here. As you can see, that's my account ID. That's why it's incorrect. So my AWS secret access key is this. And my AWS access key is this. So in case you are getting the same error, it could very much be this. So I'm going to try and just run this command again. Maybe we'll get an error. There we go. Cloudflare R2 already exists. So what I'm going to do now is I'm going to go inside of secrets. I'm going to find Cloudflare R2 and I'm going to delete it entirely like this. All right. And then I'm just going to go ahead and do this again, but this time with the correct ones and here's what you can do then and what you should do then go ahead and uh write model uh is it app list there we go model app list and find the id of the deployed one and do model app stop and give it this id and then do model app list again and all of them should be stopped. Okay? And then do model, deploy, chatterbox, text-to-speech Python again. Okay? This should be much faster than the first time, but this time, hopefully, I will not have all of those errors. So I'm gonna go ahead and still try and do this via browser simply because I wanna see if now I'm getting the proper error or not. So let me close this\nfocus on this one. So is it still the same thing? Or is this not failing because of that? So I would still be okay if this fails, but for a different reason. I expect this one to fail because we are missing the API key. Okay. So let me see. This is at 46. So it looks like I still have the same issue. Okay, I'm going to go ahead and debug further because I thought that would fix it, but looks like that still didn't fix it. I'm not sure if you have the same problem, but I think this is a good opportunity to debug something that is unknown to us. So I'm going to go ahead and show you exactly what I'm doing to debug this. Well, I debugged it and you can see I again made a mistake with adding secrets. Okay, this time, hopefully third time's the charm. I should be able to do it. I'm again going to go inside of my secrets and I'm going to delete Cloudflare R2. And this time I'm going to remove the invalid one, the AWS secret access key. So I'm purposely not cutting this part out simply because you can see these mistakes happen. Okay, let's try again. Again, I'm going to go ahead and list all of my apps. I'm going to find the ID of the deployed one I'm going to forcefully stop it and then I'm going to do model deploy chatterbox text to speech for the third time and see if that fixes it I'm going to do the same thing that I always do attempt to load it oh, that looks like it's green I don't think I've seen that before so maybe it works now we'll see I will keep track of the containers to see if there are any errors. I'm going to keep track of what this is going on with this. And let's see what will happen. And this time it works. Well, kind of. It's 404, not found. But the server is live. The server works. Let's see if I can go to forward slash documentation here. I'm not sure if this will be protected. Looks like it is not protected. Perfect. So we don't have to authorize this. And you can see our generate text-to-speech post request, and you can see everything it accepts. That's what I wanted to show you. So automatically generated Swagger here. And here's another thing you can do. So instead of docs, right? So this is just your normal URL. Try going to open API. What's the proper one? Open API dot... I can't... Just a second. I completely forgot which one it is. It's openapi.json. Why would it be .typescript.json? And in here, we get a super important thing. We get a type safe open API standard of our entire deployed Chatterbox server, which will be super useful for our super type safe Next.js TRPC server. So that's what I wanted to show you. After you have deployed, you should be able to go to this forward slash open API dot JSON, and you should be able to go to docs. So you should be able to do that. So now, how about we try generating audio? That should be fun, right? So what I'm going to do immediately is I'm going to go inside of my dot environment files right here. and let's go ahead and add the following. Along with Chatterbox API key, I'm going to add Chatterbox API URL and I'm just going to paste what we now know is fully working and fully correctly deployed Chatterbox. You can see it finally succeeded, finally no errors, meaning we are mounted to our R2 storage, which would mean that we should be able to test if this works. So how do we do that? Well, we go ahead and do model run, chatterbox, text to speech python. And again, I'm going to go ahead and copy this and I will add it to the source code simply so you don't have to figure out how to do it. So I will add use this to test locally. And okay, I'm not going to do it like that. I'm going to do it like this there we go and we're going to comment it out like this okay so comment that part out copy it comment it out again and paste it here and what you have to change here you don't have to change the prompt but you do have to change the voice key and you can find voice keys inside of your NPX Prisma Studio okay so I think I'm already running that here here I am so I'm gonna open Prisma Studio here I'm gonna go inside of voices and just pick a random voice make sure it's English and go ahead and copy just the last part or you can copy the entire R2 object key and then in here go ahead and paste it. So this will now use Aaron's voice to do this model run chatterbox text-to-speech python. So let's go ahead and let it be right now and let's just see if it will throw us an error or if this will actually generate a voice. So I'm not sure if this will render an output. we'll see if it does i think it will just do a add a file to our repository here like output or something so let's just let this be and let's just test if it works so if you enter an incorrect voice key it should throw you an error because it cannot find that uh in r2 storage for this example you don't need the access key the secret key simply because we are literally running the server locally okay so that's why you don't need it in a moment i'm also going to add another comment here so you can uh test a curl call with the access key and if it works this should be the result audio saved to temporary chatterbox text to speech so if you actually go to that file, which is somewhere in your computer, temporary, right here, you should find output.wav and you can play it and it should sound like the prompt you have added. Keep in mind that AI is non-deterministic, so it can be different every time. All right, so that works. Perfect. We have another thing we have to do and that's the HTTP test. So I'm going to add that here too. I'm going to comment it out. So all of these are for CLI things. I will see, I'm probably going to leave this here, but I will also have that in the readme file. So you will be able to find these commands somewhere. And none of these are really important for the functionality of the project. I just want to make sure that you know that the project works. So let's go ahead and just slightly modify this script by getting our real API URL. Then I'm going to go ahead and replace that this part. All right. So our URL forward slash generate, we're going to replace this with super secret key. And I'm going to go ahead and just copy a system voice. so let's go ahead and replace the voice key part so only inside of the quotes right just paste this all right let's copy this i'm going to comment it out and let's go ahead and paste it so you should have super secret key you should have a prompt you should have a voice key and it should lead to your working url forward slash generate and output dot wav and what this should do is it shouldn't fail that's the only thing we're testing right now so if you can do it from the command line it absolutely works uh for the tech for the our trpc server and all the other instances that we are going to use it in so what should happen now is it should generate output dot audio file inside of this project right here. And this is how it looks like when it's finished. And let's go ahead and find, and here we have an output.wav. So you can go ahead and listen to it. So my example worked. It said hello from Chatterbox and it did a slight chuckle. So definitely works. Okay, we can now go ahead and remove that audio file. We don't need it. And we can go ahead and focus on, well, connecting this to our trpc server. All right, so the only thing I'm going to do now is I'm just going to go ahead and change this back to something empty so I don't forget when I push this so I don't use my voice keys. All right, and I think everything else should work. Your API key here. All of this works perfectly fine. Brilliant. Okay, so I'm just going to change this to be use this to test. Carl, you don't have to modify this file at all. So it's provided from the source code. We went over to explain how it works, of course, and I just left these comments here so you can uncomment, copy it, and test it in your terminal. Beautiful. So what do we have to do now? Well, let's go ahead and use the fact that we have this openAPI.json and use it to create strict types within our project. So in order to do that, we're going to need to install openAPI-fetch. That's the first thing. That's how we're actually going to communicate with Chatterbox. and the second thing we're going to do is install inside of dev dependencies openAPI-typescript technically you could just do this with a normal fetch, right? but it definitely not as type safe so no reason to do that if we have this amazing open API from FastAPI So what we have to do now is we have to go inside of scripts and we have to create a new file here, which we are going to call SynchronizeAPI. So Synchronize-API.TypeScript. And inside of here, I'm just going to add a little comment. So this fetches the open API spec from the Chatterbox Text-to-Speech API. and generates type script types. You can use it by adding Chatterbox API URL and then run npm run synchronize API. All right. Or if you have an environment file, you just run this. Since this is just a script, feel free to go inside of the source code and copy the content simply because I'm mostly going to copy and paste this anyway. So we are importing the packages we need to work with our file system. We're importing .environment to communicate with the environment file. And then we are using OpenAPI TypeScript to actually connect to this TypeScript server and to change it, to parse it into strings. So let's go ahead and define the dir name here. And in here, we define the output path. So the output path will lead from this file inside of source and inside of types, which we don't have yet, and then chatterboxapi.d.ts. Then in here, we're going to go ahead and create an asynchronous function main. I'm going to go ahead and define API URL to be process environment chatterboxapiurl. Always make sure that you actually have it in your environment file, chatterboxapiurl. in case you don't have it we're going to go ahead and throw an error it's required if you want to generate types now we need to modify this url and add open api.json to get this format you're seeing on the left screen and i'm just going to add a console log here we are fetching the open api specs so now let's go ahead and run this function for that spec file and let's go ahead and turn it to string. And then we're going to go ahead and make sure that the output directory exists first. And if it doesn't, we're going to go ahead and create it. I'm going to go ahead and add a header comment to the file then so we understand what this is. So this header comment will simply say this file is auto-generated by scripts. SynchronizeAPI.TypeScript. Do not edit manually. instead run npm run sync API to regenerate and then we're going to show the URL that we used to generate this and the date and time it was generated at. So your users know that this shouldn't be modified by hand because it makes no sense if they do that. And then we're going to add an actual write file sync to the output path, use the header and combine it with the contents we have parsed. And we're going to log that out. Beautiful. And let's go ahead and actually execute that main function and catch any errors if they appear. Beautiful. So I just guided you through this synchronized script. But of course, you can just copy and paste this from the source code since it's not exactly, I mean, it is a learning material, but it's not mandatory, you know, to complete the project, if I can say so. All right. Now let's go ahead inside of source, lib file and let's create chatterbox client.ts. This is what we'll use to communicate with the API we just created. So let's import create client from open API fetch. Let's go ahead and import paths from types chatterbox API which doesn't exist yet. Let's go ahead and import environment from dot environment and let's export const chatterbox. It's going to use create client. It's going to pass the paths from the generated types, which we're going to do in a moment. And it's going to open an object for this create client function. We're going to pass in the base URL to target environment Chatterbox API URL. And to the headers, we're going to assign x API key to be environment Chatterbox API key. And because of this, our app will be the only one who can communicate with that deployed server. because in the Chatterbox text-to-speech Python, we are very strictly requiring the X API key. So unless the credentials get stolen, no one except your app will be able to communicate with that server, and that's what makes it safe, and that's why you don't have to worry too much about the rate limiting or anything like that, simply because you can create... I mean, you don't have to rely on your Python knowledge to do that. You can rely on your JavaScript knowledge to do that from now on because your app is the only one who can communicate with chatterbox okay your back end uh brilliant so now we're gonna go ahead and add the seed script to package json here let's go ahead inside of scripts and we're gonna add synchronize api tx scripts forward slash sync dash api dot type script make sure you don't misspell scripts or sync api so i always like to check scripts sync api dot type script looking good and then we have to go inside of dot environment file my apologies source lib environment file and in here we have to add chatterbox and it has to be required so let's go ahead and add minimum of one and we need the chatterbox api key which is also required oh we don't need minimum of one because we're using z.url here my apologies uh all right i think that should be it so what should we do now well first let's just check that chatterbox client is not throwing errors for the environment file you can see that now it works because we added those two so if you have any errors it means you have incorrectly typed this. Now we have to do this part. So how do we do that? Well, very easily, we now have a sync API script. So I'm going to go ahead and do npm run sync API, which is going to communicate to tutorial, mailing, chatterbox, basically the URL forward slash open API dot JSON. So again, this can take a while, especially if the server has cooled down, which probably is because we just wrote the entire thing. And that should generate the types folder and the actual call generated types. And then this file should finally stop being read because it should be able to load the paths and we should have a fully type safe API. So we reduce the chances of mistakes by a huge margin because you can see that, I mean, you can see the Python server, right? chatterbox, you can fail very easily by accidentally adding a typo here. So this will be super type safe using the chatterbox client instance, because we are going to use the open API type spec to make sure that we don't misspell any of these, right? It just makes sense to do it this way, because it's an industry standard way to do it. And our server is already super type safe. and just like that no more errors so now when you go ahead and call chatterbox you will be able to call only specific ones for example get doesn't work let's try post let's see you can see automatically auto suggests generate because that's the only one we have right now i'm not sure what else can we do this data temperature i'm not sure how to use it right now from the top of my head but you can see that it's already type safe, right? If I go ahead and try something like this, it will say that this doesn't exist. I mean, it won't auto-suggest it. So that's what we achieved with that type safety. Amazing. So here are all the files that we have changed. So this is an auto-generated file. You can see how it looks inside of source types. It's an auto-generated file. Do not edit manually. And in here, you can see we have every single detail You know, what does this request accept? What does it return? What kind of errors does it throw? So we are very aware of the server and it will not surprise us, right? It's almost like an extension of our TRPC procedures. Brilliant. So this was a very long lesson and we barely touched, you know, the second part, but it's an important lesson for us to do. So now let's go ahead and build the trpc procedure, which is going to communicate with our newly created and hosted Chatterbox client. So let's go ahead and make sure our app is running. So npm run dev, and we can now stop looking at model, and we can go ahead and focus on localhost 3000. So I'm going to close everything else now. The current state of text to speech is that it can accept a value here, here, and we can call generate speech, but it doesn't amount to anything, right? So what we have to do now is we have to make this actually submit because right now nothing is happening, okay? In order to do that, we need to build the generate procedure. There are no new packages we have to add at the moment. So we can immediately go inside of trpc routers. And we can just go ahead and build generations.ts. Let's go ahead and add our imports. So it's going to be zod trpc error from the trpc server package. Chatterbox from our newly created lib Chatterbox client, which will allow us to communicate with the deployed TTS. Prisma, upload audio helper from R2Lib. Then we're going to go ahead and add a text maximum length from features text-to-speech data constants. And then we're going to add create-trpc router and organization procedure from init file. Let's go ahead and export the generations router using create-trpc router. In here, we're going to first add get by ID organization-based procedure. So whoever wants to request a certain generation, first of all, what is a certain generation? That is the history tab, right? Here in the history tab you will see a list of all the previous things you have generated And when you click on it that will trigger the get by ID So the user needs to know the ID of the generation they are trying to fetch This will be a query. And it's going to be an asynchronous query with input and context here. So the first thing we're going to do is we're going to use Prisma to find a unique generation. So let's go ahead and do await Prisma generation find unique. And in the where, we're going to go ahead and use a combination of input.id referring to this, but only if also the currently logged in user's context organization ID is the organization ID referenced for that generation. So we shouldn't allow users from outside generations to access someone else's work when they don't belong in that organization. And since this one is purely presentational, right? GetById is only used to listen to a previous generation. We can omit organization ID and R2 object key just so we don't spill too much information if we don't need to, right? Let's not make a malicious user's job easier, right? Let's omit those two anyway. If such a generation isn't found, we're going to throw not found. Otherwise, we're going to go ahead and do the following. We are going to return the results of the generation found, but we're also going to add audio URL to go to forward slash API, forward slash audio, and then to generation.id. You will see what this is in a second, but I will give you a quick hint. That is the signed URL proxy. So we can't just load an audio from Cloudflare R2 and play it. We need to sign it and we need to stream it in a certain way that allows audio elements in HTML to play it. That's why we need to do it this way. So we make the backend do at least this part for us. Okay, that's it for the get by ID procedure. Now, we need to go ahead and do a get all procedure. A get all procedure is going to be an organization procedure with no input. So it's just going to be a direct query. So we only have access to the context here. And in here, instead of using find unique, we're going to use generations and find many. So what should we query by here? well, by the only thing we can, which is the currently logged in's user organization. So the user doesn't see anything that doesn't belong to them. Let's order by newly created. And again, let's omit the info that we don't need. And this one is much simpler. We can just return generations here. Why? Because the user loading this endpoint doesn't intend to listen to all of them. This will be used for this when we just need to list a bunch of generations. That's why we don't need to do any modifications. And now we come to the one we've been waiting for, the create procedure. How do we create a new generation? Well, this one is a bit bigger. The first thing we're going to have to do is create a pretty large params object. So which params should we put here? Well, all the params that we accept in our Chatterbox text-to-speech server. All of these params are expected. Now, it doesn't really matter that they are written the same right now. You can see they use different casing from us. So we can use our own terminology for this. For example, we can use text instead of prompt. Let's make it a string. Let's make it a minimum of one and a maximum of text maximum length. Then let's go ahead and add a voice ID. Let's make it required. Let's go ahead and add a temperature. And in here, I'm using maximum two, default 0.8, simply because that's the exact scenario here. Default 0.8 less than or equal to 0.0. So that's the same thing here. So that's how I'm assigning these values. Then we're going to have top B. Then we're going to have top K. And then we're going to have repetition penalty. Okay. Brilliant. So now let's go ahead and add mutation. Asynchronous. Input. Context. And the first thing we're going to do is we're going to attempt to find the voice that user attempts to use to generate something. Does this voice even exist? So we're going to go ahead and open a where, and we're going to first add input voice ID. Okay, so we know what voice ID you are trying to use, but should we allow you to use that voice? You might think, okay, just put this, right? Well, not exactly. What if the user wants to add a system voice, right? What if the user selects something like Andy, which is a system voice that doesn't belong to anyone? Well, in that case, we're going to use something called or. So the first scenario will be if the variant of that voice is system. If it is, you're good to go. But the second scenario will be if the variant is custom. and in that case we need to check the user's organization id all right i'm going to collapse this so it's easier to read so basically if system will allow it to anyone if custom only if you belong to the organization that has created it and then let's go ahead and sparingly select the info we need. We need the ID, the name, and we need the R2 object key. Then if the voice wasn't found, we're just going to go ahead and throw. Let me see if I did this correctly. I did. There we go. Otherwise, let's check if voice R2 object key doesn't exist. So this is a specific scenario where we have an orphaned voice. So voice audio is not available, meaning the generation will already fail. No need to waste GPU time calling Chatterbox because it's going to fail here anyway because we did the same thing here. Voice not found in that Cloudflare R2 mount path. Okay, and now we come to the cool part. So you can destruct data and error from await chatterbox.post. It will autocomplete the generate endpoint for you. And you can go ahead and open body here. And you can see prompt needs to be input.text. Voice key is voice R2 object key. And so on and so on. And that is the type safety benefit I was telling you about. You might think, well, okay, but we could have just been careful. Sure, you could. But this is how it's done in industry standard environments. And more importantly, nowadays, you cannot avoid agentic work anymore. You've probably been using AI to some extent. And the more type safe you are, the better results AI will produce in your code base. The more you ignore types, the more mistakes AI made. If you add AI to this code base, it will super safely communicate with this server because it can read the entire types file, right? So it knows exactly what to expect. So it's not just that it helps you. It also helps your team, but it also helps AI. So it's a overall win to be type safe. now let's check if we have any error being thrown from the chatterbox if we do well we have to throw our internal server error here as well then let's check if whatever this server has returned isn't an instance of array buffer okay if that's the case we also have to throw because it's an invalid audio response and currently we expect a buffer so that we can do this. We can create a buffer from that data and then we're going to have to upload that generation to Cloudflare R2. So let's first prepare the following. Let's prepare the generation ID and let's prepare R2 object key. Then we're going to go ahead and open a try and catch block. in try block let's go ahead and create the generation first so let's add this to our database so the data that this is going to accept is going to be the following i'm going to go block by blocks organization using currently logged in users organization text from input dot text, voice name, which is a hard-coded string, but also a voice ID. Now, I'm not sure if you remember, but that's what we wrote when we developed the generation model. Why would we need a separate voice name when we have a very visible relation with voice? Well, because of this onDelete set null. When this voice gets deleted, we decided that the generation that was done using this voice, which is now deleted, should still exist, but it simply won't have info about the deleted voice. This is where voice name comes in handy. So even if a voice is deleted, we can still show to the user this was built using that voice, which no longer exists, and it was called Andy Antonio Aaron, whatever. So that's why we are adding a voice name, a long voice ID. And now we just add the rest of the properties like temperature, top P, top K, and repetition penalty. So then when user tries to click something in history, it's immediately going to change the text to what it was. It will change the creativity, the voice variety. It will basically be a time machine to go back to exactly what was selected at the time of choosing that generation Brilliant So let go ahead and create that and let only return the id back after we create it because this will then return something we only make it return the id no need to add anything else here the only thing we didn't add right now is the upload key of the generation of the uploaded not voice, but the prompt that user wanted to hear, right? So the first thing we can do is we can populate generation ID now. So let's append generation ID to the newly created generation.id. But we still have a question of the R2 object storage. So now the reason we needed to create generation first is because the format in which we are going to store generations in R2 is generations forward slash organizations, then organization ID, and then in here, generation ID. So we need to have a generation ID before we can upload it. Otherwise, we wouldn't know to who it belongs to, right? So we are deriving from the generation ID to know the ownership and to what generation it belongs to. So let's create R2 object key, generations forward slash orgs the org id and then generation id and then once we have r2 object key we can do await upload audio with the buffer we have created and with the key r2 object key and once that is done we can go ahead and do await prisma generation update where generation ID and simply append new R2 object key after we upload. Now, these uploads are limited to like 10-20 megabytes, so because of that you don't have to really worry about this failing too often. But still, I would still recommend, you know, if you are planning to scale to millions of users, you should probably do this in some kind of background job, retry action, right? Make it a little bit more reliable. I think this can work up to, I don't know, 100,000 users, no problem, because we're limiting how short the files can be. So all of this is pretty fast. But chances are, the more users you get, the more of these orphaned generations you will have because audio upload failed, but we did this first, right? But still, if we happen to fail here, So if something happens in this try and catch, we're going to check if we have the generation ID. If we have it, it means we have already created a database record inside. So when this fails, we're simply going to assume the only thing that could have failed was this. I mean, technically, it could have been this too. but again the only thing we can do at this point if something fails is to go ahead and delete the generation we've just added to our database. Obviously you can handle this in a more elegant way with background jobs and retries but for now this should be just fine. And then outside of this if clause but still inside of the catch block let's just throw an internal server error. Great Once we do that, let's go outside of the try and catch. So still inside of the create method mutation, but outside of the try and catch here. Go ahead and check if there is no generation ID or if there is no R2 object key. It means something somewhere went wrong. We have to throw that as well. Otherwise, we have successfully created a generated audio and we have uploaded it successfully to R2. Amazing. So that is the create procedure. Now, let's go ahead and let's do this. So if you remember in my get by ID, I created this. But this currently doesn't exist. API audio generation ID doesn't exist. So I'm going to go ahead and create that now. Instead of API, I will go ahead and create a new folder called audio. And in here, I'm going to do generation ID. And then in here, route.ts. So let's go ahead and import out from clerknext.js server, Prisma from lib database, and get signed audio URL from lib r2. Why are we not doing this instead of a trpc query? Because trpc cannot produce the stream that we need right now. We need to stream this to the browser so it can be played in audio elements. So let's export asynchronous function get. We can skip the first parameter, which is a type of request. And we can immediately go to the next parameter, which are params. So the params are going to be a promise of an object which holds the generation ID, which is a string. Make sure that this generation ID 100% matches what you type in the folder name. That's how routing works in Next.js. If you misspell this, this will forever be undefined or vice versa. And there's no type safety. Okay, so you have to be careful. And this is one of the reasons I prefer adding trpc to Next.js, because I can't handle accidentally typing something like this, and then in here forgetting to do it, right? So that's why I really like trpc, because it's super type-safe. Let's open this function, and let's first verify the user. So await out to get user ID and organization ID. If any of those two are missing, let's throw 401. You are unauthorized to proceed from here. Then let's go ahead and first check. For this audio we want to stream to the browser, we are using a generation ID. Does that generation ID even exist? And we also have to await params to get the generation ID. So using Prisma, generation find unique, we are using where ID is generation ID, and the organization ID from the currently logged in user matches. So again, we are not letting anyone access what they shouldn't access. Let's go ahead and throw an error. If generation doesn't exist, then let's go ahead and see if the generation has an R2 object key. If it doesn't, so don't forget an exclamation point, audio is simply not available perhaps it is still in this await upload audio phase right so that's the scenario that could happen so it doesn't mean it's orphaned it could mean it's just not uploaded yet check again in a moment but if it is we can go ahead and do await get signed audio url which we have defined in R2, right? So we added an import for that. And we pass in generation R2 object key. Then in here, we can finally do audio response by awaiting a normal fetch to the signed URL. Let's check if the audio response is not okay. So if not audio response okay, failed to fetch audio status 502. And finally, let's go ahead and return a new response. And inside of here, we're going to add audio response dot body. And let's add headers. And for the headers, we're going to give it a correct content type. But we're also going to give it cache control. So it loads faster the next time the user tries to do it in the same session. Brilliant. So that's what's going to happen when a user loads an individual generation and attempts to play it. It will go through this proxy, which will stream to the browser and get a signed URL from Cloudflare. Because just by default, just by knowing the R2 object key, you shouldn't be able to just fetch things. So only by using our app can you listen to the audio generated. And the moment this R2 signed audio URL expires, which is in one hour, even if you manage to inspect element and steal the MP3 Vav file, it's not going to work anymore in an hour. So it's super safe and no one can leak your audio files. So you don't have to worry about that either. All right. Now let's build the interface for that. So we just did the second part, and now let's go ahead and start developing the third part. So let's start with source, app, dashboard, text-to-speech, and let's create a new folder in square brackets, generation ID, and then inside of that, page.tsx. And in here, we're going to go ahead and start with the good old import trpc hydrate client and prefetch from our trpc server. The text-to-speech page, this one, so this is a text-to-speech page to create something. That's this one. But this one, under generation ID, will be called text-to-speech detail page. So let's go ahead and first define the function and the types. export default asynchronous function text-to-speech detail page accepts params and those params are a promise with an object of generation id again make sure there are no misspellings here you can copy this and then go ahead here paste it just to confirm you didn't misspell it because otherwise it will always be undefined okay and now in here first things first let's go ahead and await params So now we have generation ID. And then we're going to go ahead and prefetch some things. So let's prefetch trpc generations. Oops, we forgot to add generations. That's a super easy fix. Let's go inside of trpc, routers underscore add. Let's go inside of generations. And let's do generations router from dot slash generations, the exact same way we did voices. and there should be\nNo problems here. All right, so just make sure your trpc routers underscore app has the generations. Perfect. And now you can do generations, get by ID, query options, and this time they're not empty because we need to specify exactly which one are we trying to prefetch. And for the voices, we still need to prefetch all of them because this will look identical to this. So this text-to-speech view will be almost identical to what we're going to build now with text-to-speech detail page, right? So we still need the voices because otherwise, how would we load the selected voice that was done during the time of the generation, right? All right, and now we just have to return hydrated client. And in here, we are going to go ahead and do text-to-speech detail view with generation ID, generation ID prop. The only problem is we don't have text-to-speech detail view. So now we're going to go ahead and develop that. Let's go inside of features, text-to-speech, not components, but views. and you can copy text-to-speech view and paste it and rename it to text-to-speech detail view. Okay. Then what we're going to do here is we're going to rename it. So text-to-speech detail view. And this one will not operate via initial values. Instead, it will operate via generation ID. Okay. so let's stop here in text-to-speech detail view make sure you have renamed the function and you have changed the props and let's go back inside of our dashboard text-to-speech generation id page and let's import this file there we go so we are done with the app folder and we can focus on building in the features folder. Okay. So now, instead of getting the initial values through props, this one will work differently. This one will be able to load the generation. So we already have used the RPC here. So what we're going to do is we're going to change use suspense query to be use suspense queries. So multiple of them, and you can remove the other one. So use suspense queries. And in here, we're going to go ahead and open an object. And then we're going to add queries. And then we're going to add the first one in the array like this. And then that will change the result here to be the following. Let me just change this to an array too. So the first one is going to be generation query and the second one voices query. So let's go ahead and add a tRPC, generations get by ID, query options ID, generation ID. Beautiful. So when you hover over generation query in here, you should see audio URL, ID, basically a single object or an error. And in the voices query, you should see custom and system. All right. So that works. Now, let's go ahead and do the following. Let's add data here. So data is generation query dot data. And for this one, let's just do voices query dot data. So slight modification. All voices can then stay the same. Fallback voice logic can stay the same. Let's go ahead and look at the resolved voice ID. This will need some changes. So this is now no longer using initial values. This is now using data. So data.voice ID. Let me just see. Data voice ID and all voices, all voices dot sum. Let's do data voice ID, but we also need to check. No, okay, I think we're good here. And data voice ID here. Great. And now for the default values, they are going to be completely different. So you can delete that and you can do this instead. Text coming from the data, voice ID coming from the resolve voice ID, temperature, from data temperature, top pay from top P, top K, from top K, repetition penalty, repetition penalty. Then there's one more thing we have to do. In case a voice in this generation was deleted, we need to do the following. We need to use the denormalized voice name snapshot instead of the populated voice relation. So the preview always shows the voice name at the time of generation, even if the voice was later renamed or deleted. So that's what I was explaining to you earlier, right? We're going to do this, generation voice. ID will be data, voice ID, or undefined in case it's deleted. But we will always have a snapshot of the voice and how it was named at the time. Beautiful. So now, let's go ahead and see what we have to do here. So this is text-to-speech voices provider, text-to-speech form. What I want to do here is I want to give it a property key generation ID. So it resets on every ID change. The default values can stay the same. This is the same. Text input panel can stay the same. Settings panel can stay the same. Text to speech form can stay the same. All right. okay so right now uh we can't really try this simply because generate speech cannot do anything but if you go ahead and go to text to speech and go to like one two three you should get an error let me just see if it's what i expect uh okay yeah it's an error because it cannot find the get by id but it doesn't throw you the generic not found that's what i wanted to check okay so we can't really test it yet but basically what we're doing is almost exactly the same as the text-to-speech view page with the difference that we are not using initial values but instead we are loading the values from the database that's the biggest difference here and we don't need default text-to-speech values import here at all. And we're not yet using the generation voice. So what I think we should do next is the following. Let me go ahead and go here. So I think we should test this out first. So let's go inside of source, features, text-to-speech components, and let's go inside of text-to-speech form. Right now, nothing happens if we submit, right? So I want to do this first so we can test out if what we developed works. So toast from Sonor is an import that we need. UseRouter from NextNavigation is something we need. Then we need useMutation from tanStackReactQuery. We also need use trpc from add forward slash trpc client. And then let's go ahead and add this. Let me just go ahead and see where should we do this. Inside of text to speech form. This is where we should do that. So trpc. then let's add the router and then let's go ahead and add create mutation to call use mutation and in here we're going to call drpc generations create with empty mutation options inside all right and then inside of the on submit we can delete this comment and we can open a try and catch Inside of the catch one, let's first go ahead and define the potential error message. So depending on the instance of the error, which we have to extract, depending on the instance of the error, we can either load the error message directly or a generic failed to generate audio. And then toast error message. So that's the try. Now let's go ahead and do, that's the catch. Now let's go ahead and do try. So we're going to call mutate async so we can await that. So create mutation uses generations create, which we just used to connect to the text to speech using OpenAPI specification. And in here, we just have to pass the parameters. So text value dot text dot trim, the voice ID value voice ID. And yes, we need to add value here. Okay. Then we need the temperature. Then we need top P. After that, we need top K. And after that, we need the repetition penalty. And then we're going to throw a toast, audio generated successfully. And using the data from this async mutation, we can push the user to text-to-speech data ID, which will basically redirect the user to generation ID, where we're going to reload that generation and pre-fill the data with all of those things. So I think we should be able to try this now. So I want you to prepare the following. I want you to have MPX Prisma Studio running. And right now we have no generations, right? So I'm going to do Hello World. And I will simply select Aaron, maybe do some modifications. I'm going to do some drastic ones just to easily recognize them to see if it's working. Okay, like this. And I'm going to click Generate Speech. This will probably take a while okay Because it actually doing the entire thing right now So it currently uploading this to it's communicating with model right now. You can see it's pending, right? So right now it's trying to generate our hello world. So if you get any errors, maybe open model so you can see why it fails. For example, if you added an incorrect inside of Chatterbox client, if you added an incorrect API key, if you maybe misspelled this or added an incorrect base URL, even though this should definitely work if you manage to create the types, right? So it's actually building that now, and then it's going to upload it to Cloudflare as well. So have Cloudflare open too. And in here, in the Resonance app, you will see a new folder called Generations. And you will actually be able to listen to the result. So basically what we previously tested in the command line interface, let me show you that, Shatterbox, which was this, right? We tested this and we tested this CURL. Oh, it works. so okay i have to just stop here we just basically created a ui for this that's what we just did we created a trpc and ui for that so now you can see that look at my url i am on localhost 3000 text-to-speech generation id and in here i have hello world i have aaron and i have these dramatic changes, right? And if I refresh, you can see that I'm on the exact same result. So if I go back to dashboard and then back to text-to-speech, it's completely empty. But if I go inside of my Prisma Studio now, inside of generations, and find the generation ID, and then go ahead and append that generation ID, I can load that previous generation. Not only that, but when I refresh the Cloudflare, hopefully, there we go, generations folder for this specific organization, someone just created an audio. Go ahead and download it and it should say hello world in the Aaron's voice. So now I'm going to go ahead and try and just do another voice style just to see if it works. And yes, subsequent requests should be faster. You can see that this one is faster. Let me refresh. there we go voice style works beautiful so what we have to do now is we have to create some kind of basic audio display here and then we're going to change it to a full-on waveform selector let's develop the voice preview panel which will appear here instead of this placeholder for generations which have an audio so i'm going to go ahead and close all of these other files I'm going to go inside of source, features, text to speech, components, and I will create a voice preview panel.tsx. Let's go ahead and mark this as use client. Then let's add some imports. So use ref, use state, use effect from React and pause and play icons from Lucid React. For the components, we're going to need a button and voice avatar. Both of those are actually, sorry, button is from Shatsy and UI. Voice avatar is our custom voice avatar component, which uses the use voice avatar dice bear collection hook. Okay, so make sure you use that one. Let's create a interface. Voice preview panel voice will accept an optional ID and a name. So let's go ahead. So that's specifically for the voice prop. If you forget what I mean here, in the views, this ID and name. That's what we'll be accepting for the voice prop. But the actual voice preview panel function, which we are going to export now, will have some different props. So let's go ahead and define the types first. It's going to accept audio URL. then the voice, voice preview panel voice or null and finally the text and then let's go ahead and extract those three perfect and let's go ahead and open up the function inside of here we're going to go ahead and define the selected voice name by checking if we have voice.name otherwise fall back to null We're going to do the same thing with the selected voice seed by checking if we have voice ID or fallback to null. We're then going to go ahead and create an audio ref, which has a type of HTML audio element or null by default using the use ref hook. We're going to create a state which will keep track of whether the audio is playing or not. By default, it will not be playing. and we're going to create a temporary use effect here which will be used to play audio. Later this will be moved to the wave surfer hook but for now let's just go ahead and create a super simple audio player. So if the audio is missing let's go ahead and stop. Let's go ahead and add a very simple handle play set is playing to true handle pause set is playing to false and ended to false as well. And for each of those functions, we can add an equivalent listener. So add event listener, play, pause, ended, and their respective functions. And in case any error appears, we can just catch that. And then make sure that you always clean up after yourself. So we have to remove all the event listeners we've just added and the bindings to their functions. So play, pause, and finally ended. And in here, we need audio URL. All right. Now let's go ahead and let's develop the toggle play pause function. Inside of here, again, let's check if we have audio. And a simple toggle will follow. If is playing, audio pause, else audio play. As simple as that. Then let's go ahead and do a return here. We're going to start with a div, which will have the following class names. HeightFull, Gap8, FlexCall, BorderTop, Hidden, Flex1, and Enlarge, Flex. Then we're going to develop the header. which is going to have a container of padding 6, padding bottom 0, and a heading 3 element of font semi-bold and text foreground with the text voice preview. All right. And then let's go ahead and add a very simple content here with, again, relative flex, flex 1, item center, justify center. This is some weird error. I don't think it should matter. Yeah. And in here we display an audio element with ref audio ref and source audio URL. Okay. And then we just have to develop the footer. The footer again is going to have a div flex flex call item center and padding six. Then inside of it we're going to have a grid with full and three columns inside. The first column will be taken by the metadata about the audio we are playing. The metadata will have a div, flex, minimum width of 0, flex call, and a gap 0.5. And inside of there, we're going to display a text within a paragraph. The paragraph will be truncated, text small, font medium, and text foreground. And now in here, we have to see if we have a selected name. So if we have a selected voice name in here, we're going to open a container once again. Let me go ahead and close it. This will be a div with flex, items center, gap one, text extra small, and text muted foreground. In here, we're going to use the voice avatar. We're going to pass along the seed by using either selected voice seed if we have the voice ID, or we're going to fall back the selected voice name. And for the name, we're going to use the selected voice name, which will use the snapshot of the name and the class name shrink0. And then let's go ahead and render the voice name. So this is basically why we are doing all of that snapshotting. So if someone loads this component, but the voice is deleted, we always have a snapshot of the voice name. So we can always display something here. And then the last thing we need to do here is the player controls. Okay. The player controls is going to have a div, flex, item center, justify center, and gap three. And let's add the first button here, which will have a variant of default, size, icon large, class name, rounded full, and on click, toggle, play, and pause. and depending on whether the state is playing is true or false we're going to display respective icons if is playing render pause otherwise render play both have the same class name fill with background color and for now after this div just add a simple spacer okay we don't have the component for this yet. So just a single self closing div like that. Great. Now let's go ahead and render this. So we're going to go back to the view text to speech detail view. And instead of rendering voice preview placeholder, we're going to render voice preview panel. Okay. So go ahead import that from components voice preview panel you can remove the placeholder now and we gonna have to to add some props here So the props are going to be the audio URL coming from the data audio URL voice generation voice and text, data.text. Keep in mind I'm doing this in text-to-speech detail view. Don't accidentally do it in text-to-speech view. In here it needs to be the placeholder and in here you don't have the loaded generation. You don't have data. Data is generation query result, okay? Get by ID result. That's why we have all of those things here. And then on desktop mode, you should be able to see this. So if I go ahead and do, I don't know, hello there, how are you? And go ahead and click generate speech. I'm going to pause. You should see the text here and you should be able to listen to the result. And I just confirmed that this works as expected. You can see the text is here, the voice is here, and when I click play I can hear this exact text. Great. The only problem is it doesn't work on the mobile. So mobile has no way of seeing any of those things. So let's go ahead and develop a very, very similar component called Voice Preview Mobile. So I'm going to copy Voice Preview Panel, paste it, and I'm going to rename it to Voice Preview Mobile. Then in here, I'm going to go ahead and change this to be Voice Preview Panel to be Voice Preview Mobile Voice. And I will change this prop right here. I'm going to go ahead and change this to be voice preview mobile function export. It will still accept audio URL voice and text. So all this is good. One important difference here. We're going to add use is mobile hook. So import hooks use is mobile right here. Selected voice name and seed say the same. Audio ref stays the same. Is playing stays the same. the use effect stays exactly the same. But let's also do the following. Let me just see. This should be in reverse. So let's do audio pause and audio current time set to zero. Okay, so kind of in reverse here. Then let's go ahead and see. We have to add one more use effect here after this one for mobile. If it's not mobile, make sure that this is always paused so you don't hear double audio. Then in toggle play pause, it should be normal, nothing special here. And in general, if audio URL is not available, just don't display this component. Now let's go ahead and change the outer div here. So let's change it to be border top padding for enlarge hidden. Then immediately here, we're going to render audio and we can we can remove everything inside for now. We will copy some things if they're similar. So let's go ahead and create a little grid here. So grid, grid columns using this specific value, items center gap four. Then let's go ahead and do a width reset using minimum width of zero. I'm going to go ahead and render the current text of this generation in truncate text small and font medium. Let's go ahead and render the selected voice name if we have it. The container for the voice will be the following. A div with class name margin top 0.5, flex, items center, gap 1, text extra small and text muted foreground. Let's render the voice avatar in here with seed which is either voice seed or selected voice name and class name shrink 0 and beneath it or should I say next to it we're going to render selected voice name within a span which is truncated. All right. And then outside of this div, we're going to render a new div with flex items center and gap two. And you can go ahead and go inside a voice preview panel here and you can copy this button. So just paste it here now. So the buttons variant will be default. Let me just fix this. and this. Let me see if I did this correctly. I did. Okay. The size will not be icon large. It's just going to be icon. On click will be the same. Is playing will be the same. Great. That's it. Now that we have voice preview mobile, let's go inside of text to speech detail view. And beneath the voice preview panel, you should now also add voice preview mobile or above it. It really doesn't matter. Import this. There we go. And it looks like something is off here because I can see it immediately here. Voice preview mobile. It looks like it doesn't have any spacing. So let's go inside a voice preview mobile to see exactly what's happening. here. So border top. Oh, hidden padding for. Okay, I did something large. There we go. And when you click play, it should do the exact same thing. Brilliant. So now it works both on desktop and mobile. Great. What we should do now is we should add the audio waveform visualizer for the desktop mode and enable the download button. Now let's go ahead and let's install a package called wavesurfer. So npm install wavesurfer.js and I'm going to show you what the version I'm working with package.json, Wavesurfer 7.12.1. Again, you don't have to be on the same version, but in case something is broken for you, it could be that some breaking change has been introduced. Now we're going to go and create a hook for initializing Wavesurfer. So we're going to go inside of source, features, text to speech, go ahead and create hooks folder here, and let's create use Wavesurfer dot ts. Now keep in mind that this will mostly be rebuilding what we just created here in the voice panels, right? But using the voice surfer library. So if you want to feel free to copy this component from the source code, simply because I'm not sure there's so much learning value here as there is just, you know, setting it up. But however you prefer, I will still go through the file to explain what we're doing. So we need callback, use effect, use ref, and use state. Let's get Wave Surfer from Wave Surfer JS and use is mobile from hooks use mobile. Then let's go ahead and define use Wave Surfer options to be optional URL and optional So, out of play Boolean, as well as on ready and on error optional functions. We're then going to create an interface. Use Wave Surfer return. And in here, let's go ahead and add container ref with react ref object, HTML div element or null, is playing, is ready, current time, duration, and then some functions. These functions are going to be toggle, play, or pause, seek forward for a specific number of seconds, and seek backwards for a specific number of seconds as well. Using that, we can go ahead and export a function, useWavesurfer. We can go ahead and assign useWavesurfer options for URL, autoplay, onReady, and error, and we can define the return method. now let's go ahead and for now I'm just going to remove the return method simply so it doesn't error so much but later we're going to add it back let's go ahead and define the container ref using use ref wave surfer ref again with use ref the container one is a div element the wave surfer has wave surfer instance or null and our use is mobile hook instance then let's define all the states that we're going to need. Is playing, is ready, current time, and duration. First two are booleans, the second two are numbers. Then let's go ahead and create a use effect. The use effect will control the wave surfer in a similar manner as our use effect did in the voice preview panel. So first things first, let's check if we can mount the waveform anywhere and if we have the URL. If we don't, we can do an early return. If we do have it, let's go ahead and make sure that we reset it entirely by destroying and restarting it. Then we can go ahead and set destroyed back to false. And then we can go ahead and create the Wavesurfer instance. In here, we're going to select to which container should we mount Wavesurfer to. And then we're going to add some colors to it. So I'm choosing these colors for wave color, progress color, and cursor color simply because they mostly match what we have in our global CSS. And then we go ahead and we match the following things. The cursor width, for example. Then we go ahead and do the bar width and we make that 2. We do bar gap and we put that to 2. Bar radius. Bar min height. height, we set this to auto, and normalize, we set that to true. Then let's go ahead and append to the Wavesurfer ref, the Wavesurfer instance we just created above, and let's go ahead and add some events. The first event will be Wavesurfer on ready. So what we should do is we should set set is ready to true and set duration to Wavesurfer get duration In here we going to add the following thing If auto play, let's go ahead and do vs.play and with an empty catch. The reason we're doing that is because otherwise you will have errors in your console. Catch not allowed error when the browser blocks auto play without user interaction. So this is a simple fix for that. And then beneath that, let's just go ahead and execute the onReady callback. Then we have to go ahead and assign all the other WaveSurfer events. So on play, pause, and finish, we are changing the hooks accordingly. On play, set is playing to true, and on pause and finish to false. So very similar to what we did here in the voice preview panel. We're just adjusting it for this package. There is another one here, which is for duration. So time update, set current time. Then let's go ahead and do the error scenario. So vs on error, if destroyed, let's go ahead and do an early return. Otherwise, let's log the error and let's trigger a callback new error, string error. Then let's go ahead and load the URL. So vs.load URL and let's catch the error. Again, if destroyed, do an early return. Otherwise, log and do an onerror, which is identical to one above. And then we need to do the cleanup. The cleanup is very simple. Return, set the destroyed to true, which will prevent all subsequent loads or errors. And do vs destroy. And the dependencies array should be the following. URL, out of play, onready, onerror, and ismobile. Then let's go ahead and define a function toggle play pause with use callback wave surfer current play pause. Then let's go ahead and define a seek forward function. Seek forward function will set the seconds parameter to be 5 by default. It will initialize the wave surfer dot current. If it doesn't exist, it's going to do an early return and it's going to calculate the new time based on the current time of the Wavesurfer, multiply it, my apologies, add seconds to it, and pass in the total duration in the second argument of math.min so it cannot go above that. And then it will use Wavesurfer seek to the new time. Now let's go ahead and do an equivalent seek backward, which is a very similar function, it just works in a different, in the opposite way. So we use math.min max and we deduct the seconds instead of add the seconds. And then again we just repeat vs seek to. We're using use callback here so we memoize these functions. And the last thing we have to do is we have to return all of those functions through return. So container ref is playing, is ready, current time, duration, toggle play, pause, seek forward and seek backward. Now let's go ahead back inside of the voice preview panel. So inside of source features, text to speech components, voice preview panel. And in here, we can now remove pause and play and we can remove use ref, use state and use effect. Actually, let's do the following. The only thing we're going to leave here is use state. And for pause and play, we are going to add some more icons. So we're going to add download. we're going to add redo and undo then let's go ahead and let's also add format from date fns here let's go ahead and import a batch component from components ui batch let's go ahead and import a spinner component from components ui spinner and let's go ahead and import cnutil then let's go ahead and import this new package. I mean this new hook we just built, wavesurfer, from hooks, use wavesurfer. So we have to go outside of the components folder, inside of hooks, use wavesurfer. That's where it's located. And then let's go ahead and develop a very simple format time function here. It accepts seconds and it returns a string. So let's go ahead and return format, pass in new date, seconds times a thousand, and in this specific format, we're using format from date FNS. Then let's go ahead and let's add one more property here to the voice preview panel. So I'm going to go ahead and add is downloading and set is downloading here in the use state. Then what we're going to do is we're going to remove audio ref, we're going to remove is playing, and we're going to remove the entire use effect. Okay, then go ahead and remove the entire toggle play pause. And instead, let's go ahead and let's add use Wave Surfer. For the URL, pass along audio URL and autoplay set to true. Then in here, let's get the container ref is playing, is ready, current time, duration, Toggle, play, pause, seek forward. Sorry, seek backward, but also seek forward, which I don't seem to have. I will see why that is happening. Oh, we do. Seek forward. Okay. And that should be it. Now, let's go ahead and let's add a function to download a file. so handle download is going to trigger set is downloading to true it will generate a safe name based on the text prop so it's going to slice to 50 characters trim it it's going to use the following regex to turn it into a safe file name and it will replace any characters that file systems don't support in and remove them otherwise it's just going to fall back to speech if you just want to if you don't want to write these regexes you can just use safe name to be like speech right this is if you want to reuse the text so the user sees exactly what this file is about and the way the download is going to work is by creating a mock element and then simply appending to that element and simulating a click on it. So we're going to add it to body, click on it, and then we immediately remove it. And then after one second, we're going to set downloading back to false. All right. Now let's go ahead and fix everything we need here. So in the content here, we're going to go ahead and remove this audio here with is ready like this. So if it's ready, what it's going to do is it's going to render a div like this with absolute, inset 0, zindex of 10, flex, item center, and justify center. And then it's going to render a batch component. this batch component is going to have the following class name gap2, bg background with 90% opacity px3, py1.5, text small text muted foreground and shadow small and inside we're going to go ahead and render a spinner with the size 4 and we're going to render a span loading audio and then outside of this, my apologies, I told you this is ready. This is supposed to be not ready. So make sure you have an exclamation point here. This is like we are loading your audio. And then otherwise, we're going to go ahead and render the container ref here where the WaveSurfer will be initialized. So this WaveSurfer will have the following class name. full width, cursor pointer, transition opacity, and duration 200. And if it's not ready, it will simply have opacity zero. The reason we cannot hide the entire element using this is because this element needs to be mounted somewhere because otherwise, WaveSurfer will never load. That's why we have to do it with opacity, okay? let's see what we have to do next so outside of this div here we have to add time display so i'm going to add flex item center and justify center i'm going to go ahead and add a paragraph here which will be text 3 excel font semi bold tabular nums because the numbers will change tracking tight and text foreground. We're going to go ahead and display the current time. Then we're going to add an empty space and render a span with text muted foreground and the divider with format time. So I'm going to add this here, format time duration. In the footer here, majority of this will stay the same. Let's go inside of player controls to see what needs to be changed here. All right, so the player's control will have a change. We're going to introduce some new buttons. So the first button here is going to be a variant of ghost, size icon large, class name flex call, and on click, we're going to seek backwards for 10 seconds. and it's going to be disabled if the audio is not ready. It's going to use the undo icon with class name size 4, minus margin bottom 1, and the span text 10 pixels font medium 10. So we can go ahead and copy this then. Go after this button to toggle play pause. Paste it here. And change it to be seek forward. And I think we don't have to do much except the redo icon here. Everything else should be exactly the same. And then one last thing we're going to do is instead of this spacer here, we no longer need space,\nwe're going to add download flex justify end a button variant outline size small on click handle download disabled is downloading download icon and download text so let's go ahead and see this now beautiful you can click anywhere you can listen to it let me refresh to see this if you want to you can add some different prompt. Let me go ahead and try and do something here. I'm going to go to text the speech. I'm going to add a longer prompt and choose some deep voice. And I'm going to click generate speech to test it out. And here we have the result. You should be able to change the specific time. You should be able to seek forward. You should be able to seek backwards. You should be able to pause, play. You should be able to download the file, go ahead and play it to see if it works. And now we just have to do the same thing, but on the mobile, but not all of it, just the download button. Okay. Because thankfully for us, mobile doesn't have the capacity. Okay. So let's just go ahead and I just want to confirm that's the only thing we have to do. So let me go ahead and find Voice Preview Mobile. Yeah, so let's go inside of Voice Preview Mobile. The only thing we're going to do is we're going to add the download icon to Lucid React. Then we're going to copy the entire handle download function from here. We're going to add it anywhere here. I'm going to do it before this early return let me see we don't need set is downloading you can remove the set timeout so this one will be simpler okay and then we just have to render the button somewhere so let me go ahead and see where we should do that let's go ahead and do it here flex item center gap 2 so right before the toggle play pause we're going to add a button with variant ghost size icon on click handle download and in here render a download icon there we go right next to it there's a download button and it works beautiful amazing amazing job so go ahead and feel free to play with this this is i would say the main point of the project i will test this myself to see if there are any visible bugs but I think we did a pretty good job with this. I love how this waveform look. I like that it's clickable and you have these cool controls. Great, amazing, amazing job. Let's go ahead and let's properly test the linting and everything. So npm run lint, npm run build, and see if it works. Great. This works just fine. Now, here's one tip for you. So inside of Chatterbox, text-to-speech, Python, we kind of leak our bucket name and account ID. When I say leak, I mean to GitHub, nowhere else. So if this is a private repository, then include Chatterbox text-to-speech. If it's not, you might want to add Chatterbox TTS.py to gitignore. Okay? So if it's a public repository, you probably want to hide it. If it's a private repository like mine, it doesn't matter, right? I mean, as I said, no one can really do much without your secret. and that's the most important part here. But you shouldn't even give them a chance to try and brute force your account ID, okay? But, you know, it should work. This function, I mean, this file right here can even be maintained in a separate repository. You don't have to keep it in this one. I'm keeping it here for simplicity's sake. Obviously, as this file grows, if you want to do proper fast API, you can do a whole file structure around it and folder structure, okay? So now I'm going to do, so this is chapter 6, okay, git checkout dash b 06 TTS generation and audio player, git add all files, git commit 06 TTS generation and audio player, and git push U-U origin 06 TTS generation audio player. Great. And let's go ahead and open a pull request here and let's review the changes. Here we have our finished CI CD. So summary, we added text to speech generation with customizable voice parameters and audio controls. We added audio playback functionality with optimized interfaces for mobile and desktop devices, download functionality, and generation detail pages to view and manage text-to-speech outputs. We also have a successfully deployed Railway instance, though if you try and do something on this web instance, it will instantly fail. The reason is we need to add new variables here. So I would recommend that you do that if you deployed on Railway the same way I did. So I'm going to go inside of dot environment here. And let me just see everything that I am missing here. I'm missing a lot here. I'm going to go ahead. It feels like I've had more of them before. Let me check if I'm on the correct one. What I'm going to do is I'm just going to copy the entire environment file and just paste it here. I will just skip removing this like this. So I'm going to keep everything in here but make sure you have skip environment validation set to true because that's not part of our dot environment. So I'm going to click update variables here and then I'm going to click deploy once again just so this works on the deployed side. About the CodeRabbit comments. So this one is a major one, which again, I'm not too sure about because our works. So it says that the course configuration is invalid because we allow origins and we are using allow credentials true. For course specification, browsers will reject responses with this combination. Since authentication here is API key based, not credential cookie based, set allow credentials to false. Okay, I will look into this and see if we have to modify it for this case. I think our works is because we access it through the API, but I will verify that for you. It also does some additional Python changes here, but as I said, I'm not too familiar with Python, so I'm really not sure. All I know is that the code currently works. In here, it's detecting that we should handle these awaited signed URLs in a better way, but that's basically the message I've been trying to get across every time we do anything R2 related, you should probably in production for scale do this in some kind of retry way, in some kind of background job and things like that. Some accessibility issues, we should probably be aware of that too. These buttons do need accessibility things. Same thing here. In here, we should probably do this. Reset player state and clear the ref when the URL is missing. So that's a potential edge case in use Wavesurfer hook. In here, our guard seek operation when the ration is zero is incorrect. So it says that it can get to infinity, which should break the API. So I will look into that as well. And this is a good one. Yes, Chatterbox client is a server only lib. So we should definitely use the server only package for it. That would be a very, very good fix. So this is definitely a major issue. So because otherwise this can leak into client files. Very, very good thing. In here, it looks like we're not cleaning up uploaded R2 objects in case post upload the database fails. So we then have an orphaned R2 object. We had this discussion, right? Again, this should be handled in some background job with some retries and things like that. Add a timeout to the upstream text-to-speech generation call. So, okay, I'm not sure what this is. I will research if we need that or not. Other than that, amazing, amazing job. Let's go ahead and merge this. We will look into those major issues in the next one. Let's go ahead and get checkout main, git pool origin main, and that will synchronize our branches. So in here, we should now be on the main branch, and we should now see here in the graph the scenario six merged to main. Beautiful. Amazing, amazing job. Great comments by CodeRabbit here and we have CICD building from railway with updated environment variables. Great. So I believe that marks the end of this chapter. We are on the main branch and we are ready for chapter seven. Amazing, amazing job. In this chapter, we're going to implement text-to-speech history as well as the general polish around this feature to make it feel complete. So the history tab will look something like this. Once the user selects history, we're going to go ahead and display all the previous generations, including the text of the generation, a voice, and the time it was made. clicking on each of these generations will redirect the user to the detail page with the generation id besides that we're also going to go ahead and implement some suggestions or prompts which users can click that are going to give them some fun texts to try we're also going to implement mobile responsiveness here because at the moment on mobile there is no way to change the settings or view history. So let's go ahead and start developing this. One thing I want to do before we start is go into Railway and show you one thing So you remember that we added environment variables right Well we did it incorrectly because we didn do it in production environment So I would suggest that you go inside of your railway project, make sure you're looking at production, and then in here, go into variables and raw editor once again. You can see that the variables are missing because the ones we have added, we have added to a temporary pull request environment. So again, remove all of them besides skip environment validation. And then just add the rest. Okay. So just make sure you have skip environment validation. That one is important. And click update variables and click deploy up here in the corner. And we will going to see this at the end of the chapter. All right. Let's go ahead and develop the history. The good news is that we already have the generations router. and inside of here we already have get all so all we have to do is build the ui so i want to go ahead and do the following i'm going to go inside of the app dashboard text to speech page in here we are currently only prefetching one thing the voices get all but we should also be prefetching all generations so we can load the history tab. Let's do the same thing inside of generation ID page.tsx. So in here we are prefetching an individual generation by ID, all voices, but we should also prefetch all other generations so we can load the history. Great. Now that we have this let's go ahead and modify our features text to speech components settings panel history right here so we're going to change this by adding trpc so let's go ahead and import use trpc and then we're going to go ahead and use suspend query to load the trpc generations get all because we are now prefetching them so we can suspense expect them. And then we're going to go ahead and render this exact thing that we have right here, no generations, only if generations.length doesn't exist. So only then do this return. Otherwise, we're going to go ahead and do a different return. So let's go ahead and write that down here. In that case, we're going to go ahead and write a div. And let me just see what's going on. Something's incorrect here. So if generations.length is not return this, otherwise return this. My apologies. Outside of the curly bracket, okay? Outside of the if clause. All right. And now in this flex, flex call gap one padding two, we're going to go ahead and iterate over the generations. And then each generation will be a link element. Okay. So I'm going to go over the class names in a second. Let's just ensure that you have imported link from next link. The href will be forward text to speech generation dot ID. The key will be generation ID. class name will be flex, items center, gap three, rounded large, padding three, text left, transition colors, and hover background muted. Then let's go ahead and add a div with flex, minimum width of zero, flex one, flex call, and gap 0.5. And inside of that div, we're going to go ahead and render a paragraph with truncate, text small, font medium, and text foreground with generation.text. Next to it, we're going to go ahead and add a div with class 10 flex items center gap one and a half text extra small and text muted foreground. And in here, we're going to go ahead and render a voice avatar. Let's go ahead and import voice avatar from components voice avatar voice avatar. The seed will look for generation voice ID and fall back to generation voice name snapshot, similarly to what we already do in other components. And the name will be the voice name snapshot and class name shrink zero. Then next to the voice avatar, we're going to render the voice name snapshot. Then let's generate a dot using a unicode at mid dot semicolon and then let's go ahead and open one more span format distance to now which you can import from date fns and inside of here let's go ahead and pass in the first argument which is new date generation created at and then let's go ahead and simply add a suffix like this add suffix true Great. So now let's go ahead and take a look at the history panel. So I'm going to go inside of text to speech here. And I'm going to click on history. And just like that, we should be able to get redirected into previous examples. There we go. Beautiful. This is the SSL warning because of our database URL string. It's not related to our code. Beautiful. So just like that, we have implemented the history tab. Now let's go ahead and see what we have to do next. At the moment, there is no way to view this on mobile. You can see I can only generate speech or listen, but I can't open any of the history or settings. So that's what we're going to do now. Inside of components, let's go ahead and add history drawer.tsx. And in here, I'm going to import history from Lucid React. I'm going to import button, drawer, content header, title, and trigger. Then let's go ahead and import a fully completed settings panel history, which now properly loads the history elements. And let's go ahead and export function history drawer. And in here, we're just going to create the mobile version of it in a nice drawer-like view. So we're just going to do the normal composition of this component. So let's go ahead and render a drawer. Let's go ahead and add a trigger. A trigger will have as child property and it's going to render a button. A button will have a variant of outline and the size of small. And the button will have a history icon with class name size 4. Then this trigger will open the following drawer content. The drawer content will have a header and finally a title of history. Now, outside of the header, we are very simply going to render the settings panel history within a div class name overflow y auto. Great. Now, let's go ahead and implement the settings drawer. So, we're going to go ahead and we can maybe copy history drawer, paste it here and rename this to settings drawer. Let's go inside of settings drawer and we're going to do a very similar thing. Instead of importing history from Lucid React, we're going to import settings from Lucid React. And let's go ahead and change the import of settings panel history to be settings panel settings from same name import. Now this one will be slightly different. So let's go ahead and add an interface, settings drawer props, optional open, optional on open change, and optional children. Then let's go ahead and change the name of this history drawer function to be settings drawer. And in here, we're going to go ahead and let me just see the mistake I've made here. So export function settings drawer, which lacks return type annotation. Okay, because I'm missing this. So open on OpenChange and children. And in here, we're going to go ahead and turn this drawer into a controlled component. and we are going to render the drawer trigger only if we have children passed. So this will be a multi-purpose drawer. So this will be the same but it's just going to be using the settings. So we can use this kind of button but we can also do it in a different way. You will see why in a second. Let's change this to be the settings title and this will be settings panel settings. All right, so a slightly more complicated drawer but nothing too complicated. Then let's go ahead and create the voice selector button which is an alternative way to open the settings panel drawer. So inside of this components file, voice selector button dot dsx. Let's mark it as use client and import Chevron down from Lucid React and use store from 10 stack react form. Then let's go ahead and import button from components UI button, drawer trigger from components UI drawer, voice avatar from components voice avatar, voice avatar, and use typed app form context from hooks use app form. Then let's go ahead and import use text to speech voices from context, text to speech voices context, and text to speech form options from text to speech form. Let's export function voice selector button. And in here, let's grab all voices from use text speech voices. Let's go ahead and create the form using use type app form context and passing along the form options. And let's go ahead and pick a currently active voice ID. So we are treating this as a form element because it's going to basically display here like the currently selected voice. and we need to treat it as a form element. It's essentially the same thing as this, right? This changes depending on what we select. It's a form element. Yes it can be pre but it also a form element So that what we doing now but for mobile mode Okay So the current voice is iteration of all voices Well I can collapse this so you can see all voices.find and we find the matching ID from the store. Otherwise we fall back to the top of the array. And let's go ahead and render a button label. If we have current voice.name, we render that. Otherwise, select a voice. Great. Now let's go ahead and return the UI. We're going to add a drawer trigger as child because it's going to be used for that. Let's render a button with variant outline, size small and class name flex one, justify center, gap two and pixel two, I mean px two, my apologies. If we have the current voice selected, in that case, we're going to render a voice avatar with current voice ID and current voice name. This is a form element. So in here, we don't have to do any fallbacks. Then outside of it, let's go ahead and render the button label within a span with flex1, truncate, text left, text small, and font medium. And last but not least, let's go ahead and render a chevron down element. Size 4, shrink 0, and text muted foreground. Now, we're going to go ahead inside of text input panel component in text to speech. And in here, we're going to add the import for settings drawer. We're going to add the import for history drawer. And we're going to add the import for voice selector button. Okay, so make sure you have all of those. and then let's go ahead and find the mobile layout. Here it is. And then above the generate button here, we're going to go ahead and render the following div, flex item center and gap two. And then we're going to render a settings drawer that is opened by clicking on the voice selector button. And then beneath it, we're going to render history drawer. and this is what that's going to look like. So from here, users can open settings on mobile and they should be able to open history. They can. Okay, looks like it was a glitch. Perfect. And on desktop, you can see none of that exists. So we have now officially done both. Users can do the same thing on mobile that they can do on desktop. I just want to do a few tests. So I should be able to change the voice and it immediately changes. Perfect. And if I use history to switch to a manual, there we go. That changes to perfect. So it works as expected. Now let's go ahead and build prompt suggestions for desktop scenario. So I'm going to go inside of source, features, text to speech components. And in here, I'm going to add prompt suggestions.tsx. Prompt suggestions are mostly a matter of copyright. I mean, yes, copywriting, like marketing stuff. So feel free to open my source code and look at this file because I will be adding a bunch of text info, which doesn't make sense to pause and write when you can just copy. So to build this component, we're going to need book, open, smile microphone languages clapperboard gamepad 2 podcast and brain from lucid react then we're going to import badge from components ui badge we're going to import the type lucid icon from lucid react and here comes the part where we have to build uh the prompt suggestions so prompt suggestions are going to be an array of objects each object will have a label a prompt and an icon, okay? And this will look like this, for example. Narrate a story, prompt, and then a text, and icon book open. And these will actually be very similar to quick actions, right? We have something called quick actions in source features dashboard data. And the same way you copy them from source code here, you can copy from source code here. So it's located inside of source app features, text-to-speech components prompt suggestions. So I'm going to go ahead and add all of the other ones. There we go. All of these. You can of course choose how many of them you want to have. And now that you have that, let's go ahead and build the prompt suggestions function. So function prompt suggestions will accept on select with a prompt string, which returns a void and let's go ahead and return in here we're going to go ahead and render a div with class name space y 2.5 we're going to render a paragraph which says get started with and then we're going to list over the suggestions so this was text small text muted foreground this is flex flex wrap gap 2 and we're going to go ahead and iterate over the prompt suggestions. So for each suggestion we're going to render a badge element with key suggestion label variant outline class name cursor pointer gap 1.5 py1 px 2.5 text extra small hover background accent and rounded medium. On click, we're going to call on select and suggestion dot prompt. And then we're going to render the icon, which you can access through suggestion prop dot icon, give it a class name of size three shrink zero. And next to it, we're going to render a label. Great. Now let's go ahead inside of the text input panel once again. And this time we are aiming for the desktop layout here and let's go ahead and find where it says get started by typing or pasting the text above and instead of that paragraph we're going to render the prompt suggestions so let's go ahead and import prompt suggestions and we have to add on select so when user selects a prompt what we want to do is we want to call form set field value and modify the text field by adding the prompt value. So let's go ahead and click on text to speech so we have a completely empty text and this time instead of that blank text you should see all of your prompt suggestions. If you start typing we get rid of them but before you have anything you can select them. I think this is a very nice touch and it looks much more polished here. One last thing to do is to add a loading skeleton. So you can't really see, but it takes a while to load the text-to-speech page. You can see that when I click on it, it's not instant. There's like a millisecond I have to wait when I click on text-to-speech, especially if I clear cache and then click. So one way you can improve that is by adding a loading file in Next.js. So this is happening because inside of dashboard text-to-speech page, you can see that text-to-speech view is immediately calling use-suspense query, meaning that we are blocked from seeing this component being rendered until generations get all and voices get all are loaded. So because of that, let's go ahead and create a text-to-speech loading.tsx, which is a reserved file name. And in here, you basically write your very own loading skeleton. So for example, a paragraph which says loading, like this. And you can see that now, clicking on this page from the navigation bar is instant, right? But the problem is it has this very brief loading state. So what I suggest you do, since there's not much knowledge in learning how to build loading pages, In fact, you can do most of it with AI now. You just tell it to look at the page and say, build me an equivalent loading. I would suggest that you just copy loading pages from my source code. We're going to do that by using the skeleton component. And we are basically going to replicate all of the components we can see in the three layout panel. All right. So this is the entire file. Again, you don't need to have this file. It works fine without it. It just makes it a little bit more polished. So we're using the Skeleton from Components UI Skeleton, and I'm reusing the Voice Preview Placeholder because it is a placeholder. It's technically a Skeleton, right? And I'm just replicating, I mean, AI replicated what the three-panel layout looks like. We are replicating this, this, this. You can see that if I try and refresh, this is what it looks like. And you can see how good this looks. So it's not everything in Skeleton. And now clicking on text-to-speech is instant, and it has a very smooth skeleton. Again, you don't have to do this if you don't want to. Brilliant. So let's go ahead and confirm npm run lint works, npm run build works. And now let's go ahead and git checkout b07 text-to-speech history and polish. git add, git commit 07, text to speech history and polish, git push u origin 07, just like that. Now let's go ahead and merge this pull request. Since this was a super simple one, we really don't have to wait for a review because it was mostly just UI components, a skeleton, and a few prefetches here and there. we are aware of mostly all of the changes. No new business logic was introduced here so we don't have to wait for the review in this case. We can just go ahead and merge the pull request anyway. And you will still see the most up-to-date on your railway because once you have merged to main this will trigger another redeployment on the railway. So if you go there you should see your new project being built right now. There we go, it's building. So you will see all the changes up to date. Brilliant. Now let's go ahead and do git checkout main and git pool origin main And what I like to do as always is just double check that everything okay So I the main branch and inside of my graph I can see 07 history and polish being merged back into the main branch Brilliant. Amazing job. And see you in the next chapter. As our app grows, errors will inevitably happen and we need to know about them. We'll learn how to integrate Sentry for error monitoring. First, by demonstrating what an unhandled error looks like, and then by setting up Sentry with their one command wizard, we are going to be able to track every single error that happens within our app. So this is a bonus chapter, meaning that the rest of the tutorial will work fine without it, but it's a great addition to any production app, and I'd highly recommend following along. So this is the current state of our app. Before we integrate Sentry, we have our app UI, for example, text-to-speech page. We hit the generate button. The TRPC mutation throws an error, and it's lost forever. There's no alert, and there's no record of it happening. Only the user who has bad experience knows that it happened. But what we're going to add now is Sentry, which is going to capture every single error that happens within our app. And we're going to get the complete stack trace. We're going to get a session replay. We're going to get context. And we're going to get logs. And this will make us be one step ahead of all the errors that can happen for our users. So let's see how that looks like. Right now, I have my app running on npm run dev. And I am on my text to speech page. So if I go inside of the generations procedure right here in the tRPC folder, and if I choose the create mutation, and if I block the entire mutation by throwing an error like this, you can see the rest of the code is now unreachable. What happens? Well, for the user, it fails. You can see the error down here. This is a test error. But for us, we don't really know that it happened, right? So the only thing that's left is bad user experience and the user churning away because we weren't aware of this error and we cannot fix it unless the user decides to report this error. All right? So now, let's go ahead and learn how to fix this behavior. By using the link on the screen, you can go ahead and create a Sentry account. and using the code CodeWithAntonio, you will be able to get Sentry Team for free for three months as well as 150,000 errors. Once you create an account and finish onboarding, you will have to create your first project. In case you already have a project, you can go ahead and click on your icon, Projects, and in here you can find the same button. Go ahead and select Next.js as the platform. I'm going to leave the defaults as they are and I'm going to change the project name to be a resonance app and I'm going to add it to my team and I'm going to hit create project. Then I'm going to go ahead and copy the installation wizard. So let's go ahead and shut down our server and let's add the wizard command right here. This will take us to the login screen so don't be surprised if this happened and then in here you will see login complete and it will automatically pre-select the project you have just created. After Sentry Next.js package is installed with your preferred package manager, you will be asked a couple of questions about the configuration of Sentry. For example, do you want Sentry to route requests in the browser through your Next.js server to avoid ad blockers? You can select yes for that so you don't miss out on any important monitoring or logs. Same thing for tracing to track the performance of your application. Same thing for session replays because they are an amazing video-like reproduction of errors during a user session on the front end. And finally, let's enable logs to send your application logs to Sentry. Now it's asking us if we want to create an example page, Sentry example page. Go ahead and select yes for that too. And finally, do we have a CI-CD tool? We do. It's Railways. So select yes for that and go ahead and copy the SentryAlf token. Once you've copied the token, go ahead and immediately add it to your .environment file so you don't lose it. So I'm going to add it at the top here. Then we can go ahead and select yes, continue. For the MCP server, we can select no simply because we are not using AI in this project. If you are, feel free to select yes. and that's it that's the entire wizard and error monitoring has now been added to our project with one simple command so i'm going to go ahead and do npm run dev once again and let's go ahead and look through the files that were just created so we have some configs here in the root of our project inside of source we have instrumentation for the client but the ones we can actually see are the Sentry Example page and API Sentry Example API. So let's go ahead and visit the Sentry Example page. So I'm going to go to localhost 3000 and I'm going to go to Sentry Example page. And in here, I'm going to go ahead and throw an error, which is essentially the same thing that we tested when we started this chapter with attempting to create a generation, right? there we go an error just happened but this time the error was reported to sentry so when you go inside of your project now you will be able to see that issue right here so let's wait a second for it to refresh there we go so 24 seconds ago an exception was raised you can see we have one api error which happened but we also have a front-end error simply because that's how that Sentry example page was made to work. So let's go ahead now and add Sentry configuration to our trpc server. So the way we do that is, well, actually, I'm going to show you the documentation so you learn how to do it yourself. So by clicking on help here, you can find the documentation. And in here, you can search for trpc middleware right here. So no need to install anything else. You just need to import sentry and let's go inside of our source trpc init.ts and at the top here I'm going to add import all as sentry from sentry node. Then in here let's create a sentry middleware. So I'm going to go ahead and do that right here after we initialize trpc instance. Here it is, sentry middleware, like that. And then let's go ahead and do the following. Let's find the base procedure and let's modify the base procedure to use the sentry middleware. And then since this t.procedure is essentially the same as base procedure, let's go ahead and replace that to be base procedure. And let's do the same thing for our organization procedure here. So now, all errors that happen within the out procedure or the organization procedure or any procedure in our app is also going to be logged to Sentry. So let's go ahead and do some examples. Inside of trpc, routers, generations.ts, again, find the create procedure. and inside of the mutation here, I'm going to throw new error. Something went wrong like that. And let's try this one more time. So now I'm going to go ahead onto localhost 3000. I'm going to go inside of text speech and I will just write something randomly and click generate speech. So again, something went wrong. But the difference is that now this will be tracked in the Sentry monitoring page. And just like that, you can see that 20 seconds ago, we have a TRPC error happened inside of our app. And in here, you can see the entire stack trace behind it. You can even see the source map. So you can see which line of code triggered that, which is amazing in itself. So you don't have to look at, you know, bundled minified code. You can see the breadcrumbs of how it happened. So you can see it was a get request. You can, I mean, it started with a get request and then how it led to the button clicking, which essentially triggered a post request to TRPC. And finally, you can see the exact generation, which has failed. But you can extend this even more. So you can see that you have a billion information about the user to whom this happened. But you can improve this even more by something called sentry logging. Sentry logging basically enables you to add consistent and human readable information. For example, let's go ahead back inside of our procedures here. the rpc routers generations and i'm going to go ahead at the top and i'm going to import sentry and then i'm going to go ahead and find the create procedure i'm going to remove this error now and for example once i start generating something right here i'm going to go ahead and trigger a sentry logger info generation started. And I'm going to map which organization started it, for which voice ID, and what was the text length that they were trying to do. And then let's go ahead and go down here after we skip these errors and let's log another one. Actually, let's not do it here. Let's do it after we successfully created the generation here. let me go ahead and find it after we successfully update it here let's do another logging audio generated for this organization for this generation ID or if it fails for example\nright here, let's go ahead and add a log here. If you want to, in the catch, you can open up error, and then in here you can get error.message, or you can just skip that for now and just keep track of the logger.error, right? So now, if an error happens, you will be able to know exactly for which organization this happened and for which voice ID. So perhaps inside of your analytics later, you will be able to deduct like, hey, 90% of the errors that are happening are happening for a specific voice ID or for a specific organization ID or for a specific text length. So maybe the text length is the problem. This is how performance and errors in general is being solved in production grade instances, right? In industry standards. So for example, I'm going to, let me just refresh just in case and I'm going to select something here and I will click generate speech and we're going to go here into century and we're going to see how this looks like now when we have some logs. So after the audio has generated once the request has actually finished you can go ahead inside of explore and click on logs and in here you will see that you now have a log generation started and in here you can find all the information that we sent along for example the organization id right here and then you can go ahead and click on audio generated and you can see the generation id and the organization id as well and cool thing about sentry is that it's very aware of the information it needs to combine along. So if it starts generating something like this, so if we do generation started, and then an error happens, and in here we log an error, for example, which will be caught here in the error, it will intelligently connect the organization ID and the voice ID to what it was previously set here. So you will basically have a very good mental map of what went wrong. So logs in combination with just your baseline error monitoring with Sentry is an extremely powerful feature. And what we just did in like 10 minutes is just scratching the surface of what Sentry can do. I honestly cannot recommend Sentry enough for anyone who wants to you know, improve their error logging and go to production in general. It's absolutely a must have. I have personally been running Sentry on my website for almost two years now. And it's definitely my go to whenever something, some anomaly is happening, right? You can also set up email notifications, Slack notifications, Discord notifications, whatever you prefer, and your entire team can be alerted whenever you have an error that you don't expect to have. And you can immediately get on it. You can turn this into a real task. You can assign it to one of your teams. You can set the priority. You can even track this in GitHub, Jira, whatever you're using. Basically, go ahead and explore this a bit. I'm sure you will be super impressed by everything it can do. And it's a great addition to a project like this with lots of moving parts and lots of things that can go wrong. Great. So what I want to do to end this chapter is just clean up after ourselves. So we no longer need century example page. We can remove that and we don't need century example API. Everything else would be a good idea to keep. So then where it comes to your dot environment file, we should copy the century out token we should go to railway.com and we should find our project we should go inside of the variables open the raw editor and simply add the new century out token here okay and then click update variables and click deploy here once again brilliant so i'm going to open a pull request and the branch for this too. As I said, this is a bonus chapter. So even if you didn't do this, you will still be able to continue with the tutorial. Don't worry. So I'm going to go ahead and check out bonus century error monitoring. I'm going to add all changes, bonus century error monitoring and git push u dash u origin bonus sentry error monitoring then let's go ahead and let's open a pull request and this time we will be able to merge it immediately simply because all of these was added by a command line interface so there's no need to review anything here let's simply merge our pull request. Great, so that was a super quick chapter on how to improve your application with error monitoring, having structured logs, session replays, basically an addition that is a must-have for any serious project. Amazing job, and see you in the next chapter. And before we do that, I almost forgot, git checkout main and git pool origin main. Otherwise, we would have to do that in the other chapter. I always like to confirm that everything's fine here. Let's just open GitHub graph. There we go. Bonus merged into main. Beautiful. Amazing, amazing job. In this chapter, we're going to build the entire dedicated voices page where users can browse, search and manage all available voices. We're going to build the full browsing experience, meaning voice cards with avatars, flag emojis, category badges, plus a debounced URL-based search. We're then going to add the create voice dialogue, where users will either be able to upload via drag and drop or record and see the audio feedback in a waveform preview in real time using Wavesurfer and record RTC libraries. We're going to wrap it all up by enabling users to delete any custom voices and doing cache invalidation after that. By the end, users will have a complete CRUD functionality for custom voices. So let's get started by building the voices page. So make sure you are on the main branch. In the previous bonus chapter, I almost forgot that we have to check out the git checkout main and then git pull origin main. So in case you didn't do that, make sure you do this so your main is up to date with whatever we have merged. then make sure you have npm run dev running and let's get started by building the voices page so I'm gonna go ahead inside of the source folder here app dashboard and I'm gonna create a new folder called voices in here I'm gonna need a page.tsx and I'm gonna need a layout.tsx So let's go ahead and add a very simple voices page function. So inside of page here, so voices page, we're going to export the default asynchronous function voices page. And in here, let's just return a div voices page. Then let's go ahead and export the layout as well. So layout.tsx is going to have a singular children prop, which are a type of React, React node. And the layout is going to return a component, VoicesLayout, which we don't have yet. So let's go ahead and build it since it's very simple. I'm going to go inside of Features and I'm going to create a new folder called Voices. We actually have it already, my apologies, but we only have data inside. That's why I forgot. And inside, let's create views. And in here, let's create voices layout.tsx. And this will be a very simple component. Our voices layout will be a normal export function. So no default export here because this is a component. It will accept the children prop. It will have a div, flex, height full, min height zero, flex column, and overflow hidden. And it's going to reuse our page header shared component and render the children beneath it. Then let's go back inside of the actual app dashboard voices layout. And let's import voices layout from the respective features, voices, views, voices layout. By now, you should be able to go to localhost 3000. and you should be able to click on the explore voices, which should redirect you to forward slash voices. Make sure there's no typo and you should see the voices header where you can open the sidebar on mobile and on desktop, it should be the same thing except optimized for desktop and you should see a text which says voices page. Brilliant. Now that we have that, Let's go ahead and focus on building the Voices page, since right now this is just a text here. So I'm going to go back inside of the features, Voices, Views, and in here I'll create a new file, VoicesView.tsx. I'm going to go ahead and mark this as UseClient, and I'm going to export a function called VoicesView. So inside of here, I'm going to go ahead and return a container with some classes, which will mostly be some spacing. So flex1, space Y10, overflow Y auto, padding 3, and enlarge padding 6. And now in here, we have to render the voices toolbar, which will hold the search component. and we have to render the voices content, which will render different voice lists All right So in order to build the toolbar component we need to add a package called NUCs because our search will be controlled via URL, right? So in order to make this search functionality box work, we need to modify the URL. So let's go ahead and install NUCs. and while we are here let's also install use debounce so we can properly debounce whatever the user searches without overloading our trpc procedure and server now that we have nooks installed i will quickly show you the version so nooks 2.8.8 again you don't have to have the same version i'm just showing this so you are aware of my versions and use the bounds 10.1.0. Now I'm going to go ahead inside of the voices folder here and I'm going to create a new folder called lib. And in here, I'm going to prepare the params that users will be able to control. So those params are going to be the query param. So let's import create search params cache and parse as a string from nooks forward slash server. And in here, we're going to export voices search params and give it one property, query. And that's going to be a type of string. And it's going to have this property called with default. So whenever the URL resets to this, it will clear the URL, right? I will show you what I mean once we actually have the functionality for that. And let's do export const voices search params cache, create search params cache, and pass in the voice's search params. So by using Nuke's server package, we can now do Nuke's functionality both in client components, but also in React server components, which we are going to need in order to prefetch if a URL has a query. So we have to think about that as well as all the other things, okay? So now I'm going to go ahead and connect these params to our page here in voices. So find voices page.tsx. And let's go ahead and do the following. We're going to add some imports now. I'm going to add metadata from next and search params from Nuke's server. Both are importing a type, all right? Then let's go ahead and define the metadata to be voices. So now when you're on your voices page, take a look at the tab, it should say Voices Resonance, exactly what we want. Then let's go ahead and extend the props here. So the props will be search params, and the type of search params will be a promise, and then search params inside, like that. And then what we have to do is we have to somehow extract the query from the search params. So make sure the function is asynchronous. And then let's do query await voices search params cache, which we can import from features voices lib params and parse the search params. So this will actually parse and see if the query string is valid. and if it's valid we can then do prefetching to voices get all so we need to import prefetch from trpc server and we need to import trpc from trpc server as well and you can see there's no errors happening because if you command click onto voices get all we already took care of that So our get all procedure for voices has always had the ability to be searchable, right? And we are now using that in this prefetch function. So this React server component will prefetch all voices for this page. But in case this page is not loaded on localhost 3000 voices, this is easy. We just have to prefetch get all. But what if it's prefetched with something like this, right? in this case we should only prefetch for this query that's why we needed to do this all right now that we are here let's also add hydrate client and then let's go ahead and replace this with hydrate client and in here we're going to do voices view and you can import voices view from features, voices, views, and finally, voices view. All right. So now I'm going to go ahead and go back inside of the voices view component here. Right now, it's just an empty div, so you can add a paragraph to do list of voice cards, and then you should get a text list of voice cards. So I'm going to go ahead and develop a voice card component. So let's go inside of voices, open a new folder, components. And in here, I'm going to create voice card.tsx. Let's go ahead and import link from next link. And microphone, more horizontal, pause and play icons from Lucid React. Now let's add the components. Button, drop down menu, drop down content, drop down menu item and drop down menu trigger. let's continue by adding spinner from components ui spinner let's go ahead and add voice avatar from components voice avatar then let's go ahead and add import type infer router outputs from trpc server let's go ahead and import the entire app router type from trpc routers underscore app and let's go ahead and import voice category labels from features, voices, data, voice categories. So it's basically a remapping of our Prisma and Neum to human readable strings. We already use this for seeding. Now we need to define a singular voice item type by using infer router outputs, passing in the entire app router and then targeting the voices router, specifically get all procedure and then we already did this somewhere else. We can choose between custom or system and then just pick a single item from that array because getAllProcedure returns custom and system voices. So in order to have fully type safe type, we need to have this info, what's being returned. And that's what we just got right here, all right? And then we can safely create an interface for the card. Voice is a voice item type. Now let's go ahead and prepare region names using the INTL API, display names, English in array, type region. Then I'm going to go ahead and create a function parseLanguage. ParseLanguage function will accept the locale, and it's going to split the locale with a dash. So let me try and find what I mean here. It is inside of voices data. Voices scoping maybe. Voices, let me try. Voice categories. Okay, I know where it is. It's in the seed script. This, it's going to look for this. It's going to find English US, English IN, English GB, and it will basically separate by the little dash here and then it will get the country in the second arguments. That's why we skip over the first. In case we cannot find that argument, it means the locale is broken. So we're just going to return an empty flag and the region locale. Then we're going to try and create a flag emoji. You can use an npm package for that, but looks like it can be done by using this specific code right here. In case you don't want to type it out, feel free to go inside of the source code and just copy this function. But yes, basically it will accept the following and using this, it will generate a emoji of that country. So that's what this function is doing. I'm going to leave it to you to pause the screen and copy it. And now let's go ahead and develop the actual voice card. So function voice card has a voice right here. Let's go ahead and extract flag and region using parse language, voice.language. Let's go ahead and define the audio source to go to API voices and then encode URI component voice ID. so this will now go to a function which i don't think we yet have we will create it later but it's going to be identical to audio so it will basically just stream the r2 object key we have of the voice sample so very similar to this route it will just return the response of assigned audio all right uh great now that we have that let's go ahead and let's return some code so we can actually see this so we're gonna go ahead and add a div with flex item center gap one overflow hidden rounded extra large border pr3 and large pr6 then in here another div relative height 24 width 20, shrink 0, large height of 30, enlarge width 24. Then in here, we're going to go ahead and just add a self-closing div because this will serve as a nice little decorator, which will kind of have the card. You're going to see how that looks like. Absolute left 0, top 0, height 24, or width 10, border right, and background muted of 50. On large, height is 30, and on large, width is 12. Then let's go ahead and create a container, which is going to hold our voice avatar. So that's going to have absolute, instant zero, flex item center, and justify center. In here, let's go ahead and render the voice avatar, and let give the voice avatar a class name size 14 border 1 pixels border white and shadow extra small large size 18 Great. Then we have to go outside of this div here and we have to create the container which is going to hold the voice name and all the other things. So flex, minimum width of zero, flex one, flex call, gap 1.5, enlarge gap three. Then let's open another div here. Flex, item center, gap 1.5, line clamp one, text small, font medium, and tracking tight. And this is where we render the voice name. Next to the voice name, we're going to go ahead and add a self-closing span, which will serve as a decorator. So size 1, shrink 0, rounded full, BG muted foreground 50. And then next to it, we're going to render the voice category inside of a span. This span will have this specific text color, and it will look through voice category labels to find a match here for a human readable category using voice.category. Great. Outside of this div, we're going to render a voice description in a paragraph with line clamp one text extra small and text muted foreground then let's go ahead and render a flag and a region in another paragraph flex item center gap one text extra small span shrink zero which will render the flag emoji and span truncate font medium which will render the region text outside of this div we're going to go ahead and enter add the container which will hold the actions to play or delete something right so this and this let's go ahead and start with a normal button now i'm going to go ahead and add a few variables here which we don't yet have so const is loading will be false const is playing will be false as well and i think those are the only ones we need at the moment so let's just have them here because later we're going to create a hook that we will be able to use to extract the properties from. So this button here is going to have the following attributes, variant outline, size icon small, and class name rounded full. It's going to be disabled if we are loading it, and on click for now is there's going to be an empty arrow function. I'm just going to add to do change to toggle play, because we don't yet have this. And then inside of the button, depending on the state of is loading or is playing, we're going to either display a spinner, pause or play, and all of them will have the same class name here. Great. And then the last thing we have to do is we have to add a drop down menu, which will serve as the options button where we are going to be able to delete the voice from. So the drop-down menu will have the drop-down menu trigger, which has an as child property, which will render a button with a variant of outline, size icon small, and class name rounded full. And that button will simply have a more horizontal icon with a class name of size 4. Great. Outside of the drop-down menu trigger, we're going to add drop down menu content with align end option, then drop down menu item as child. And in here, we're going to render a link, Ahrefs text to speech with predefined voice ID of voice ID with a microphone icon of size four and text foreground and span use this voice. So later, this is where we're going to add the delete option as well. Now let's go ahead and build the VoicesList component, which will be used to render the newly created voice card. So VoicesList.tsx is going to have audio lines, microphone, and volume 2 icons, and it's going to use voice card from voice card and the type voice item from voice card. So make sure you're exporting the type voice item and make sure you're exporting function voice card. Okay. Now in here, we're going to create an interface for the voice list props. And let's go ahead and export a function voices list. First thing we're going to do is empty state. So in case there are no voices, we're going to go ahead and display a placeholder here. Let's make sure to return it actually. And the placeholder will actually be super similar to voice preview panel. So it's going to basically be, oh, not voice preview, voice preview placeholder. So basically this idea of three icons next to each other. So that's what we're going to be building now if you want to visualize it. So inside of this space Y4, we're going to add a heading which will render the title of the current list. Then beneath it, we're going to add a container, flex flex call, item center, justify center, gap three, and padding Y12. Then inside, we're going to add a div, relative flex height 14 with 32, item center, and justify center. And then in here, we're going to render the three icons. So the first icon is going to be volume 2 icon within absolute left 0 minus rotate 30 rounded full background muted padding 4. The volume 2 will have a size 5 and text muted foreground. And then we're going to have two more items here. So the first one is this one the microphone icon which has relative z index of 10 rounded full bg foreground and padding 4. microphone has a text of background and bg of foreground for the container so the reverse of what we had here where it's bg muted and text muted foreground and the last one is identical to the first one with a different icon and then what we're going to do is outside of this div here just render a paragraph saying no voices found and then beneath it what voices will appear here using the title. Be mindful of the classes so maximum width medium, text center, text small, text needed foreground, text large, font semi bold, tracking tight and text foreground. So that's for a scenario of no voices but for a scenario of voices existing we're going to go ahead and do a much simpler code so space y4 then an h3 element rendering the title and then we're gonna have a very simple grid grid columns one on mobile on large grid columns two and gap four between each item and we're simply going to iterate over the voices using voice.map and render a voice card and give each card a key and the entire voice property. The voice comes from the data right here. Perfect. So now that we have that, we can go ahead and try and render it in the VoicesView page. So why can we already render it? Well, because we are already prefetching it, right? Instead of app, dashboard, voices, page.tsx, we prefetch the voices. So now, instead of this VoicesView right here, we can go ahead and prefetch them. So how do we do that? Well, by creating something called VoicesContent. So right here, I'm just going to separate it for clarity's sake, a new component called VoicesContent. And we're going to start by adding trpc here. So let's import trpc. then we're going to go ahead and import the data. Well, not import the data, extract the data from use suspense query from TanStack React query. And the use suspense query will simply accept the RPC voices get all. And for now, let's just make query options be empty. So data will have custom and system voices now. and then what we have to do is we have to return two voices lists. One voice list for team voices, which will render data.custom and one for this built-in voices, data.system. Make sure you've imported voices list. And then simply render the voices content. So I'm going to go ahead and replace this with the voices content. Like this. So let's go ahead and refresh. And just like that, you should be able to see that there are no team voices, but there are many, many default voices here which are built in. And by clicking on use this voice, you should have it pre-selected in the text to speech. Great. So what's missing right now is the preview button. We currently cannot play it and we cannot search through our voices. So let's add that. Let's go ahead and let's add the ability to search through available system voices. For that, we're going to go inside of source app folder layout file. And we're going to go ahead and import Nooks Adapter from Nooks Adapters Next app. And then let's go ahead and wrap our children inside of the Nooks Adapter. this will then allow us to revisit our voices view page but this time we will be able to actually query so we're going to go back inside of the features voices views voices view right here and let's go ahead and add a query state from Nooks right here. And let's go ahead and track it. So const query comes from use query state, which will accept query key in the first parameter and use voices search params from lib params, which we created here So make sure you not using the cache that for the React server component This one is for the client component And now we can keep track of the query from the URL So inside of the query options here, we can pass along the query just like that. So already, in theory, this should work. If I modify my URL to search for Aaron, so I appended a query Aaron here, only Aaron is available. Great. Now we have to connect that to UI. So in order to do that, we have to modify the voices view to also render the voices toolbar, which is currently not something that exists. So we have to go ahead and build it. So we're going to go inside of components and we're going to create VoicesToolbar.tsx. We're going to import useState, useQueryState, useDebouncedCallback, search, and sparkles. For the components, we're going to import button, inputGroup, inputGroupInput, and inputGroupAddOn. and let's also import voices search params let's go ahead and export function voices toolbar and in here we're going to add our query so query and set query from use query state with use which uses the query key inside of voices search params dot query so from here we can also control it, not just look at it. We also need a local query so we can immediately show what the user is typing, but only debounce into set query after a certain time has passed. So use debounced callback so that set query is only called after 300 milliseconds has passed and user hasn't typed anything new. meaning that we can consider this the end of typing. Because if you search for every keystroke, that can very easily be tough on the database. Or even if it isn't tough on the database, it's just unoptimized. So let's go ahead and add a container here. Let's add a div. And in here, let's go ahead and add an H2 element with text large. on large devices text to excel font semi-bold and tracking tight and then beneath it let's go ahead and add a paragraph discover your voices or make your own then outside of this div let's add a flex flex call gap three then let's add a flex items center gap three and in here we're gonna add the input group with class name on large max with small. Let's go ahead and add input group add-on which will render the search icon with class name size 4 and then beneath it or should I say next to it an input group input with placeholder search voices value of local query and on change set local query immediately, but debounce set query from NUCs, so it's not immediately queried to the database. And then next to the input group, let's add a div ml auto, which is hidden on mobile, and only visible on desktop. And in here, we're going to render a button size small sparkles custom voice. And then let's do the opposite outside of the div. So only visible on desktop, let's do the same thing. The reason we are separating it now is partially because of the class name, but also because it's going to have different wrappers around this button once we actually implement the dialog and the drawer for it. So now we can import voices toolbar from components voices toolbar. And just like that, you should be able, you can see it's already preselected here. And if you search for Andy, after 300 milliseconds, it will change. You can see that while I'm typing, nothing is happening. But if I stop typing, only then does the database input happen. And you can see that right now, the query is this. But if I delete it, the query is completely removed from the URL. so it's clear. Great, beautiful, beautiful job. So let's go ahead and see what else we have to do. We have to implement the ability to play this and we have to add placeholder, dialogue, and drawer for this button right here. So let's create the proxy so that we can play audio samples. So we're going to go inside of source, app, API, and in here we're going to create voices. Inside of voices we're going to create voice id and then inside of voice id we're going to create a route.ts in here let's go ahead and import out prisma and get signed audio url let's go ahead and export a get request which skips over the first argument and only looks for params with voice id make sure the voice id isn't misspelled otherwise this will be undefined let's go ahead and check if we are allowed to look at this request. If we have an organization, should I say, then let's extract the voice ID from params. Let's attempt to find the voice. And then let's go ahead and do some checks if the voice doesn't exist. And then if we aren't allowed to listen to this voice, or if the voice simply is orphaned, and we don't have the equivalent storage object key. otherwise let's create the signed url and let's create an audio response by using fetch if fetching the audio response fails we also have to throw an error otherwise let's go ahead and prepare the content type which we can either get by looking at the audio response headers content type or fallback to audio wav and then let's go ahead and return new response audioresponse.body with the headers, content type, content type, and cache control right here. And by doing that, you should be able to, well, you can't play it yet because we didn't implement the hook to play that. So now we have to add that hook inside of here. Hooks, let's go ahead and do use audio playback dot ds. And this will mostly be repeating what we already have in the voice preview mobile so we already kind of did a bunch of things here and it's going to be no different than that so it's not really much of a learning experience so what i would highly suggest is that you look inside of my source code go inside of source hooks find use audio playback and simply copy the entire thing here so we mark it as use client we import all of these hooks we set up the audio ref is playing and is loading we go ahead and do a cleanup here we add a toggle play button we go ahead and return is playing loading and toggle play so as i said we already did something like this a few times so it really makes no sense for you to write it by hand once more if you want to here is the entire code of course but it's a very simple is playing is loading and toggle play now let's go inside a voice card right here so we can replay i mean replace these fake ones with a real hook so the way we're going to do that is by removing these entirely make sure you have audio source and then go ahead and import use audio playback from Hooks use audio playback and let's go ahead and use toggle play here in the to-do which we have assigned it to. Great so now you should be able to play this so try playing Aaron for example. All right so I tested it and it works and now let's go ahead and implement the voice create dialogue so we can prepare for adding custom voices. So we're going to build this inside of features, voices, components, voice create dialogue dot t s x. Let's go ahead and add all the imports, use client, everything from dialogue, and everything from drawer and also use is mobile hook. then let's go ahead and create the interface voice create dialogue props which has optional children optional open boolean and optional on open change let's go ahead and export function voice create dialogue with the props assigned and let's start by checking if we are on mobile or not so if we are on mobile we're going to use a drawer otherwise we're going to use a dialogue so let's go ahead and let's return a drawer let me go ahead and end the return here make sure you pass in the open and on open change properties to the drawer here and if we pass the children along make sure to render it inside of a drawer trigger with as child prop like that and then in here we're just going to add a mock composition so drawer content drawer header, drawer title, and drawer description. As simple as that. And we're going to just do the equivalent for desktop mode. So in here, let's go ahead and return dialogue with open on open change. If we have children, render the dialogue trigger with as child. And the composition is exactly the same, except in here we're using dialogue header, dialogue title, dialogue description with a class name text left, right? You can see it's exactly the same, but different components. The composition is the same. And in here we have a slight text left. Great. And now that we have this, let's go ahead inside of the voice toolbar and let's go ahead and encapsulate each. So I'm going to encapsulate this one with voice create dialogue and I'm going to encapsulate this one with voice create dialogue as well. Let's go ahead and import\nPort voice create dialogue. Let me show you from .slash voice create dialogue because they are in the same folder. So this is voices toolbar and this is voice create dialogue. Let me go ahead and fix the indentation. And now let me go ahead and click on custom voice. You can see on mobile you have a drawer, but on desktop you have a dialogue. Brilliant. So that was step one here. We did the Voices page, browse, search cards with avatars and flags, and now we have to develop the file upload and voice recorder, and finally, voice deletion. In order to create the upload functionality, we have to install some packages. React Drop Zone, Locale Codes, and Music Metadata to help us define the duration of the audio file, content type, basically all things we need to validate if it's a valid input or not. Then let's go inside of source, app, API, and inside of voices, let's create a create folder with route.tss inside, and the route should be a file, so route.ts. Let's go ahead and add all the imports that we are going to need. I'm going to expand this. Auth, parse buffer, Zod, Prisma, upload audio, voice categories, and type voice category from Prisma client. So all the things we already have and have worked with. Let's create the voice schema using Z object. Required name, required category, which has to be an enum of one of the categories we support. Language, which needs to be required, needs to be a language code, and description which can be optional. Let's go ahead and define some other limits. For example, maximum upload size will be 20 megabytes. We don't want this to be too long. And minimum audio duration will be 10 seconds. Then let's go ahead and export this post request. And the reason we are doing this inside of an APA route and not trpc is again because of buffers and how you upload files so we need to do it instead of a normal route for this specific functionality let's extract user id and organization id from await out let's go ahead and throw an error if it's missing then let's go ahead and parse the URL and let's go ahead and see if we have all the valid fields from this schema here. So why are we parsing the URL? Why not request JSON of the body? Because we're going to be using a file upload. So when you do file upload, you can't pass along the body. Well, you can, but then you have to change the type of upload and you have to look for the compatibility and all those other things. It's simpler to simply append to search params, right? So let's go ahead and use create a voice schema to initiate safe parse over URL search params for name, category, language, and description. So make sure you don't misspell any of these fields here. And then if the validation has not been a success, let's throw an error, invalid input, and we can pass along all the issues that happened during parsing with a status of 400. Remember to add an exclamation point here. So this is if it fails. Otherwise, we are safe to extract that data. So we now have proper types for each of these fields here because we have parsed them and thrown errors otherwise. Because of the current request, which I was explaining earlier, we can now do request array buffer and we can get the exact file buffer. that we are trying to upload. First, let's check if it exists. If file buffer byte length doesn't exist, this isn't an audio file. So let's go, I mean, this isn't a valid file. So let's throw an error. Next, let's check if it's too big. If the byte length is above maximum upload size, which we've defined to be 20 megabytes, let's throw an error once again. Then let's go ahead and extract the content type from request headers get content type. And if the content type is missing, let's throw an error as well then let's go ahead and normalize the content type simply because it can have some additional properties here so we're just going to fall back to this or we can just trim it here all right then let's go ahead and let's validate the audio format and the duration so we're going to go ahead and define the duration to be a number and then we're going to go ahead and open a try and catch block here. Inside of try, we're going to go ahead and define the metadata to be a wait parse buffer, passing the new unit 8 array with the file buffer inside, give it a mean type of content type, which we have, and duration to be true. And then to this duration let, we're going to assign metadata format duration or fallback to zero. and in catch we're going to throw an error file is not a valid audio file and then using that duration we can go ahead and throw if it's not long enough so if duration is less than minimum audio duration in seconds let's return response json audio is too short let's display how long it is minimum duration is minimum audio duration in seconds all right then let's go ahead and define a created voice ID and let's go ahead and open a try and catch block for creating the voice ID. So we are going to create a new voice using Prisma voice create, pass along name, variant custom, pass along the org ID, description, category language, and let's only select back the ID. Then we can immediately assign created voice ID to be voice ID. And using that, we can create the R2 object key. Voices, organizations, organization ID, voice ID. So, so far, we've only had voices system. If you look inside of the seed script, somewhere here, you will find our key, which was voices system. them. So when an organization is creating their voice, we're going to store that under voices, organizations, and then organization ID. Now that we have the R2 object key, we can go ahead and upload audio, pass in the buffer, R2 object key, and content type to be normalized content type. And finally, once that is uploaded, we can go ahead and update that voice and pass along the R2 object key. Now in the catch method we have to do some cleanup. So if something failed let's go ahead and check. If we have created voice id let's delete it simply because that would mean something went wrong. And let's go ahead and return response json failed to create voice please retry. Otherwise we're going to go ahead and return response json with the status of 201 pass along the name of the voice and message voice created successfully. Great. Now let's go ahead and create the UI. So let's go inside of features, voices, components, and let's create a new file voice create form dot dsx. In here, let's go ahead and let's add use client, use state, zod, toast, use form, use drop zone from our new package, use mutation and use query client. Then let's go ahead and let's add all the icons which we're going to use and there's a lot of them. Audio lines, folder open, X, all the way to a line left. So pause and add all of these icons. Then we're going to add locales from locale codes. Then let's go ahead and import CN from lib utils, but let's also add one more function to lib utils here. So go inside of source lib utils where we only have cn and let's go ahead and add a function format file size, which accepts the bytes. And if it's less than 1024, it will return this in that format. if it's less than twice that it will return it in kilobytes otherwise in megabytes so a very simple function to display format file size and then once we have that we can go ahead and import that from here as well so format file size then let's go ahead and import our previously created use audio playback. Let's go ahead and import use trpc. For the components, we're going to add the following ones. Button, input, text area, field, field error, tabs, tabs list, tabs trigger, and tabs content. Besides that, we're also going to have select and all the elements inside of select. Then we're going to have popover and these three elements from popover. After that, we're going to have command and all of the elements from command. And last, we're going to have voice categories and voice category labels from features, voices, data, voice categories. Great. Now, let's go ahead and define the language options for the drop-down. That's going to use the locales.all filter for each locale, find a tag, and find if the tag includes a dash, and then display locale name. After we've filtered that, let's map over each locale and give it an object of value and label. Value will be its tag and label will look for location and then render the name and location in parentheses. Otherwise, fall back to just the name. So that's how our language options will look like this. It's going to be an array of value and label. And we're using locales from our locale codes package. Great. let's go ahead and let's create voice create form schema which is a Z object which accepts a required name a file which has to be an instance of file an audio file is required And we just add nullable and refine here to make sure it required. Additionally, we add category, language and description. The only optional thing here is description. Great. So now let's go ahead and let's build the voice create form. So for that, we're going to need to add voice create form props optional scrollable optional footer and optional on error here so let's go ahead and export function voice create form with those props let's go ahead and add use drpc and query client from use query client then let's go ahead and do a create mutation so what happens when we actually hit upload right in here let's go ahead and add a mutation function. Let me go ahead and do this. So this mutation function is going to accept name, file, category language and description. We can go ahead and create the equivalent types for all of that. Here they are. The only optional one is description and be mindful of the file to be the file property. And what we're going to do is we're going to append those things to params. So params, new URL search params and pass in name, category and language. And only if description exists, pass along the description too. So this is what we're doing. We're appending those form elements to params. So later in create.route, we can destructure it from the params. So that's why we are constructing the URL element here and then parsing it against the voice schema. So name, category, language, and description is what we're adding here. Name, category, language, and description. So that's what that is. And then what we can do is we can create a fetch request to that endpoint. All right, so I'm just going to pull this down. So await fetch, open parenthesis, open backticks, API voices create and attach the params. And we have to pass along a method to be post method and headers. We have to make that content dash type file dot type that was uploaded and body file. Then let's go ahead and check if the response is okay or not. If it's not, We're going to go ahead and await response JSON and throw new error body.error or fallback to failed to create voice. And last but not least, let's go ahead and return response JSON. Great. So that is our mutation here. Now let's go ahead and define the hook for form. it's going to have default values of empty name file null as file all fall back to null category so null as file or null that's what i meant to say category is going to be general as a string why general well because inside of the data for voices for not voice scoping voices data voice categories has general here and we need to use the key rather than the value so the ones that are inside of prisma schema so inside of your schema here you have an enum of categories and we're going to put general as the fallback the default language will be english us and description will be empty now let's add some validators which will trigger on submit We're going to call voice create form schema from above. And then in here, we have to open the on submit function. The on submit function will have access to value. So make sure you destructure that. Make sure it's an asynchronous function. And let's go ahead and open a try block and a catch block. Inside of the try block, we're going to call await create mutation mutate async. So we're going to use this mutation we've just created above. And in here, we're going to go ahead and simply pass along all the values. Name will be value.name. File will be value.file. And you can add an exclamation point here at the end because we know we're going to have it at this point. Category, language, and description, which can be undefined otherwise. Then let's go ahead and add a toast that we have created a voice successfully. if that passes. And let's use query client invalidate queries and invalidate by a key trpc voices get all query key. So when you create a new voice, these voices will be invalidated and refetched so you will have a new voice appear here instantly. All right. And after that, let's make sure to reset the form. Inside of the error here, let's go ahead and create the error message. which we can either do by reading error message if the error is an instance of error. Otherwise, let's fall back to a string like this. And then we're just going to go ahead and either call an error callback or simply throw a toast error. So if we have on error prop, we're going to pass along the message. Otherwise, toast.error. Great. Now let's go ahead and build the actual form. so the form element is going to have on submit which prevents default and calls form handle submit and the class name which is going to have a cn util for class name flex flex call and if it's scrollable it will change the css to minimum height zero flex one otherwise gap six then let's go ahead and do the same thing for the inner div when it comes to class names like this. So if it's scrollable, we're going to use no scrollbar, class name, flex, flex, column, gap, six, overflow, y, auto, px4, otherwise a much simpler class. And let's go ahead and add some fields here. So the first form.field, which will have a name of file, will render the following element. First things first, let's get if it's empty or not. I mean, if it's invalid or not. So fieldStateMeta is touched and not fieldStateMeta is valid. We're going to use that for some ARIA attributes. Let's go ahead and... Oh, something's wrong here. I have to reverse these or not. Let me just see what's wrong with the... All right, I need double curly brackets. In the return here, let's render the field element, data invalid is invalid. And then in here, we're going to render the tabs with the default value of upload. So our tabs will have a tabs list with the following class names and two options. The first tab trigger will have a value of upload, an icon of upload, and a text of upload. And the second one, which for now is going to be disabled, will be the value record with a microphone icon and record label. And then in the tabs content, outside of the tabs list here, for the value upload, let's add a to-do file upload like that. and if it's invalid let's go ahead and render an error here so if it's invalid simply render a field error like that all right so that's the first element and now we have to add all the other fields but before we continue developing this i think it would be a good idea to actually render this so let's go inside of voice create dialogue because this is where this is going to be rendered do the desktop mode so desktop is quite easy after the dialog header simply add voice create form like this and import it from voice create form the mobile one will be slightly different so after draw a header render voice create form but we're going to do it with some props so it's going to have a scrollable prop and it's going to have a footer prop and the footer is going to use drawer footer drawer close from components ui drawer button from components ui button and i think that's it let me just check if i imported all of them correctly footer close and button make sure there are no imports from base ui or erratics so it needs to be all from components UI. Great. That looks good. Let's keep it like that for now. And if you go ahead and click on custom voice, you should start to see the results here. You can see we now have to do file upload and a hidden record tab. So if I go to desktop, it's the same, but on desktop. So at this now we can see what we are developing. So let's go back to create form and let's go ahead and implement the second element which is going to be the name of the field. So after we end this form field let's go ahead and open a new fourth field which is the name of the audio file. Inside of this, let's go ahead and render a field. Let's go ahead and define what's the invalid state as usual, and let's go ahead and return. Inside of the return, we're going to go ahead and render a field element with data is invalid for the invalid prop above. Let's add a div with relative flex items center, then a div with pointer events none, absolute left zero, flex height full with 11 items center and justify center. And within, we simply render a tag element like this. Then let go ahead and render an input down here after that tag which has an ID of field field name placeholder voice label is invalid value field state value on change field handle change event target value on blur field handle blur and class name PL10 to leave space for this little tag right here. Great. So we now have a voice label at the bottom. Beautiful. And let's go ahead and make sure we can display an error if it happens. So identical to how we displayed here. Okay, now let's go ahead and do category field. So form field for category, we're going to start in an identical way as we did above. So you can copy and paste the is invalid state because it's the same. And now let's go ahead and return a field once again. And inside of the field, it's actually going to be the same div. So you can copy this too, right? And let's add it here. And we're going to need to add a closing div here. Instead of the tag icon, it's going to be layers. All right. and then outside of this div we are going to render a select component. Select is going to have a value field state value and on value change field handle change. In here let's add a select trigger. The select trigger will have a class name with full and pl10 and select value inside will have a placeholder select a category. Outside of the select trigger we have to render the select content with all the options. Select content will iterate over voice categories and for each category it's going to display a select item with the value of that category and then mapping the voice category label to each category. So audiobook becomes this, customer service becomes this. You can go ahead and try it out to see if it works and there we go. you have all of the options right here. Great. Now let's go ahead and make sure that we have an is error field here. And let's wrap it up before we start building some custom fields with a description component. So for the description component, you can copy the entire form field for the name because it's very similar. So I'm going to copy this. And I'm going to go to the end of the category here, paste this. I'm going to change this to be description. I'm going to change the icon here to be align left. And instead of using an input here, I'm going to be using a text area. The placeholder is not going to be voice label. It's going to be describe this voice. On change will be identical, field will be identical, but class name will be different and along with class name we're also going to add rows three now let's go ahead and let's add a submit button so again after we end with this form dot field right here we're going to open a form dot subscribe form dot subscribe is going to have a selector like this and then in here, let's go ahead and use that selector. And then let's go ahead and render a submit button inside of a constant. So submit button will be a button with type submit. Disabled is submitting. If it's submitting, we're going to display creating, otherwise create voice. And then we're going to choose if we have a prop called footer, then we're going to whoops we have to do this outside of the submit button constant if we have a footer we're going to render a footer and then submit button inside otherwise submit button solo so you can see how that looks here we have a cancel button for the drawer that's why we need this solution all right but for desktop it's much simpler we don't need cancel here because we can cancel in other ways All right, so we now have that, and now we need to create a language combo box, which is basically a component for choosing languages. So I'm going to go here at the top, and before we create the voice form props, let's go ahead and create a function language combo box. Make sure you add the following props, value on change and is invalid. Let's go ahead and start with a open and set open hook. Let's go ahead and choose a selected label using the language options. So from that language option, if we find a valid value, we're going to pick its label. Otherwise, fall back to an empty string. And for that, we're going to use a popover and command component. so let's go ahead and return a popover with open and on open change calling open and set open respectively let's go ahead and render popover trigger with as child property and inside of here we're going to render a button now this button here is going to have type of button variant outline role combo box this type button is super important so you don't accidentally submit the form with it. After these area elements, we're going to add a class name using CN, height 9, width full, justify between, font normal, and if there's no value, let's go ahead and make it muted. Then inside of this button here, we're going to go ahead and render a div, flex, item center, gap 2, and truncate, and we're going to render a globe icon with size 4, shrink 0, and text mid-foreground. It's a self-closing tag. If we have a selected voice, I mean language, we're going to display the selected label, which is calculated up here, if we can find it in the language options array. Otherwise, select a language. Then let's go ahead outside of this div and render chevrons up-down with size 4, shrink 0, and opacity 50. And then let's go ahead and go outside of the popover trigger and let's render the popover content. The popover content has this specific radix class name so it fixes the width of the trigger and padding zero. In here we are going to do command composition. So let's add the command input so we can search for a language. Command input is a self-closing tag. Then we render a command list. If there's nothing to be found within the list, we're going to add command empty. And instead of the command group right here, we're going to go ahead and iterate over the language options. Inside of these language options, we're going to go ahead and render the command item. Each command item will have a key, value, and on select. Let me just go ahead and see what I need to fix here. Just a second. like that. So key is language value, value is language label, all right? And on change, we send language dot value, and we call set open pulse to close this. Instead of the command item, we are rendering language label. And for each label that's selected, we're going to render a check icon. So by default, ml auto and size four, but only if the value is equal to language value, we check the opacity to 100 and opacity to 0 otherwise. All right. So that is the language combo box component. Now we have to render the language combo box component before we render the description. So find the category, find the description, and between those two, go ahead and add form field, name language, Go ahead and open your usual field property. Your usual is invalid state. Go ahead and return the field property. And instead of rendering text area or anything like that, we render language combo box with value, field state value, on change field handle change is invalid prop, and the usual is invalid. So identical to what we've been doing in description, but even simpler because inside of the field, we directly render our new language combo box. So go ahead and save that file. And now in here, you can see that you can select a voice and you can even search for, for example, Croatian voice right here. Beautiful, beautiful work. Now it's time to add the drop zone so we can actually upload a file. So we're going to go above our language component here, above function language combo box and in here we're going to develop function file drop zone. File drop zone component will have prop file on file change and is invalid prop. Then we're going to use our use audio playback hook and pass along the file prop to extract is playing and toggle play. And then we get to the big hook. We can now use use drop zone. We use use drop zone from the package we installed react drop zone right react drop zone right here and from this package uh we can go ahead and extract a bunch of things but we first have to define how this drop zone is going to work so it's going to accept audio files only we're going to calculate the maximum size to be 20 megabytes on the front end as well we're going to disable uploading multiple files and we're going to specifically handle on drop this way. So accepted files and on file change, only the first one from the accepted files. So we will never accept multiple files. And then once you have done that, you can go ahead and extract get root props, get input props, is drag active and is drag reject. All right. Now inside of this function here, we will have two renders. The first one is if we have a file. So if we have a file, we're going to display one thing. If we don't, we're going to display the other thing. So in case we have a file, we have to go ahead and display a container. Then we have to render an icon within another container here. So the outer container is flex, item center, gap three, rounded, Excel, border, and padding four. The inner one is flex, size 10, item center justify center rounded large and background muted And inside of file audio of size five and text muted foreground Then next to it we going to go ahead and add some file information So minimum width of 0 and flex 1. In the first paragraph, we're going to render a file name inside of truncate, text small, and font medium class names. And then we're going to use format file size function. so in the paragraph beneath it we render the file size with extra small text and text muted foreground all right then we're going to add a button to listen to what we just uploaded type of button is super important so you don't accidentally submit something and on click toggle play let's go ahead and add if it's playing pause icon otherwise play icon and another thing we need to do is a reset button. So if we want to reset what we just uploaded, again, a type of button and on click on file change to null with an X icon. Great. And then the last thing we have to do for this component outside of this curly bracket. So the normal return is the usual drop zone. So let's go ahead and add this. And we'll just prepare this empty div. And we're going to go ahead and pass all the props which arrive from a function get root props so we extract that from use drop zone here and then we have to open the dynamic class name here inside of the dynamic class names let's first add the default ones so the default class names will look like this flex cursor pointer flex call item center justify center gap for overflow hidden rounded to excel border px6 py10 and transition colors. Then we're going to do a nested ternary in the second argument. If is drag reject or if is invalid, we're going to add border destructive so the user knows you can't drag and drop that file. Otherwise, we're going to check if the drag is active and we're going to use border primary or simply fall back to nothing. Then we have to render a native input element with get input props spread inside. Beneath it, we're going to go ahead and render audio lines icon within a container, flex size 12, item center, justify center, rounded Excel, and background muted. Then beneath that, we're going to add some text. So flex, flex call, item center, gap 1.5. Let's go ahead and render a paragraph. Upload your audio files with the following class names. beneath it another paragraph supports all audio formats maximum file size 20 megabytes with those class names so some more informational content and then outside of this div a button again type button super important variant outline size small with folder open icon and upload file text. Great. We are ready to render file drop zone. So let's go ahead and go inside of voice create form. And this one will be a little bit different. So we have to find the tabs here. And in here, tabs content for the value upload, we're going to go ahead and remove the placeholder and render a file drop zone just like this. So now when you click here, you should be able to upload your file. And now you can try it out. So I'm going to go ahead and try and upload something here. If you click on the play button, you will hear audio feedback of what is played. You can, of course, remove it too. And now let's go ahead and try and upload this. So I'm going to call this custom voice number one. I will pick narrative. I will leave this as English custom voice test, and I will click create voice. It looks like audio is too short. So there we go. Our validation works. Let's try with a proper voice. And after you pick a voice which is long enough, for me that was Madison in case you're interested, you will have your custom voice right here. There we go. And you should be able to use it here as well and you can see how it's separated into team voices. So you can actually try and record yourself and upload that file and you should be able to clone your own voice right now. So what we have to do next here is we have to enable you to record your voice directly from here rather than just uploading a file. Also a quick tip if you're working with larger files like up to 20 megabytes you actually have to go instead of nextconfig.ts and in here you have to open experimental and then proxy client max body size and change it to 20 megabytes otherwise you will get errors in the console. But since we are really looking for small files here this will almost never be a problem. We only need 10 seconds and this could maybe trigger you to restart your server so let's just go ahead and do that and see if everything works just fine with that. There we go, looks great. In order to build the recording functionality we have to install some packages. So it's called install recordRTC and types for recordRTC. Then let's go ahead and let's build the component voice. Sorry, we're going to build a hook use audio recorder first. So let's go inside of features of voices. And that will be a hook specifically for this. So hooks and inside of here use audio recorder.ds. All right. and inside of this hook we're going to go ahead and import the following packages so i'm going to expand this as much as i can use state use ref use callback use effect import type record rtc type from record rtc wave surfer and record plugin from wave surfer great let's go ahead and export function use audio recorder let's go ahead and all the state fields we need so is recording elapsed time audio blob and error be mindful of the types boolean number blob or null string or null then let's go ahead and add all the refs that we're going to need we're going to need a recorder ref which is a type of record rtc type stream ref which is a type of media stream timer ref which is a return type of set interval container ref which is a div element vs ref which is a wave server instance and microphone stream ref which will simply be a custom object with on destroy function here all are initialized as null in the default let's go ahead and create a destroy wave surfer function then let's go ahead and implement a cleanup function which will reset everything so timer ref not current reset recorder ref not current destroy stream ref current get tracks for each track stop a track and destroy wave surfer which is a function we've created up here okay now that we have that let's go ahead and create a use effect inside of here let's go ahead and see if we can early return we can do that if it's not recording if we don't have a container or if we don't have a stream ref container then let's go ahead and initialize Wavesurfer. We've already done this before. Wavesurfer create, pass in the container, wave color, and then some configuration properties. Let's go ahead and assign the Wavesurfer instance to its ref. And then let's go ahead and add a record plugin. So record VS register plugin, record plugin create, scrolling waveform set to true. So we get the desired effect. Then let's go ahead and listen to the microphone. So handle record, render microphone stream using streamref.current and go ahead and assign that to the ref as well and when it out mounts make sure to destroy the wave surfer and in the dependency array we need two items is recording and destroy wave surfer then let's go ahead and let's develop the start recording use callback let's go ahead and open a try and catch block let me go ahead and properly close this. There we go. In the try block here, first of all, let's reset our elements, set error, set audio blob, and set elapsed time to zero. Then let's go ahead and create a stream by awaiting navigator, which is a built-in browser API, get the media devices, and then get user media and search for audio media. So audio set to true. And then let's go ahead and assign that to a ref. So stream ref becomes the stream. And then let's go ahead and import record RTC dynamically. So we are awaiting import record RTC and we go ahead and extract record RTC and stereo audio recorder from here. Once we have dynamically imported this, we can go ahead and create an instance of recorder using new record RTC, pass along the stream and pass along the other options it requires. Then let's go ahead and assign that to a ref. Let's start the recording and set is recording set to true. Now to keep track of time, we also need to define a start time and then set an interval to timer ref to modify the elapsed time every 100 milliseconds. Let's go ahead and catch an error if anything goes wrong inside of the initialization here. So I'm just going to go ahead and check what type of error happens here. So if error is an instance of DOM exception and error name is not allowed error, we're going to set the error to microphone access denied. Please allow microphone access in your browser settings. Else, fail to access microphone. Please check your device. And the only thing we need in this dependency array is a cleanup function. and let me just see we also of course need to call cleanup in the catch method my apologies, there we go and now let's go ahead and implement stop recording function which is simpler stop recording simply accepts on blob function which is optional it attempts to find a recorder ref if it cannot find it, it cannot stop it otherwise it calls stop recording function it gets the blob of the recorder, sets audio blob Set is recording to false, does the cleanup, and calls on blob. This basically means user has finished recording and wants to see the results. That's why we need blob. So this isn't a cleanup function. This is a on change function, rather. Now this is a cleanup function, a reset recording function. Calls cleanup, set is recording.\nRecording to false, set elapsed time to zero, set audio blob to null, set error to null with the cleanup dependency key. And this hook will return is recording, elapsed time, audio blob, container ref, error, start recording, stop recording, and reset recording. Now, let's build a UI. So we're going to build this component in the same place as the voice create form. So I'm going to go ahead and do voice create, my apologies, voice recorder. that's going to be the name voice recorder and let's go ahead and add the icons microphone square rotate x file audio play and pause and then let's go ahead and add all the other elements we're going to need cn format file size button use audio playback use audio recorder which we've just created let's go ahead and add a custom function called format time which will simply take in the the seconds from the audio recorder and format it in a human readable way. Feel free to copy this function from the source code as this text is kind of hard to look at or you can just pause the screen and copy it yourself. Then let's go ahead and define a function voice recorder. The voice recorder function will accept file on file change and is invalid. Is invalid is optional so be mindful of that. Now let's go ahead and reuse our old use audio playback and accept the file and pass along is playing and toggle play here. Then from use audio recorder we can extract all of the things that we are exporting from here. Is recording, elapsed time, audio blob, container ref, error, start recording, stop recording and reset recording. Now let's go ahead and develop a very simple handle stop method. This will stop recording as we expect and you can access the blob from there and then it's going to create a new file instance from that blob name it recording.vav and it will call on file change as if this file was uploaded. So that's how that's going to work. Let's go ahead and also enable a handle re-record to change the file to null and reset the recording. Now if an error happens we need to have a screen for that. So let's go ahead and return the following. A div with flex, flex call, item center, gap 4, rounded to excel, border, border destructive with 50% opacity and background destructive with 5% opacity, px6py10, a paragraph describing the error inside of text center, text small, text destructive, and the button, again, type button, super important, variant outline size small, and on click, reset recording. Now let's go ahead and do an instance if we have a file. So if we have a file, meaning we've finished recording, we have to display it in a similar manner to what we did in, let me go ahead and find it, voice create form. In fact, the file drop zone might be identical to what we need. So let's actually copy this. I think it's almost identical. And I'm going to copy it here and I'm going to paste the entire thing. And now I'm just going to compare if that's what we need. So file audio definitely stays the same. File name stays the same. the only thing we have to do is after format file size we also care about how long this duration of this is so we have that info here but we don't have that info when we upload a file so we can use audio blob and if elapsed time is above zero we can go ahead and add this dot and then use format time function which we have defined above to format seconds into human readable format there we go then in here let's go ahead and see what we should do so type is button variant is goat icon is small on click toggle play let's say the title is playing to be pause and play reverse icons and beneath it let's go ahead and do the following so we should have some different buttons here. Let's go ahead and add one more button. So before we do like X button, let's add a rotate CCV button, which calls handle re-record. Make sure it's a type of button. And then we're going to do the exact same thing for the X button. So it should also click handle re-record. All right. Now let's go ahead and render if is recording state. So if it's recording, we have to go ahead and render the beautiful waveform, right? So for that, we're going to go ahead and render a flex, flex call, overflow hidden, rounded to Excel and border. And then we're going to render a container inside with a class name with full. This container is where the actual waveform will be rendered. Then flex item center, justify between border top and padding for. and beneath it we're going to format the elapsed time to see how long has it passed so you know you have passed 10 seconds. Text is 28 pixels, font semi-bold, leading 1.2 and tracking is tight. And then beneath this paragraph we're going to go ahead and render a button which simply has... Let me go ahead and indent this properly. So this button has a square icon and stop and on-click handle stop. You can go ahead and look at the props like this. So it's cleaner. All right. And now we just have to build the final UI here, which is to present this component. So let's go ahead and start with the parent div, which uses the dynamic class name, flex, cursor pointer, flex call, item center, justify center, gap four, and these classes as well. And if it's invalid, it's going to be border destructive. We then have to render the icon, which is a microphone icon within a div flex size 12 items center justify center rounded excel and background muted then let's go ahead and let's render a div flex flex call item center gap 1.5 a paragraph record your voice another paragraph click to record uh sorry click to start capturing audio be mindful of the class names of course text base font semi bold tracking tight text center text small and text muted foreground and then outside of this div let's go ahead and render a button to actually trigger the recording so this button type of button variant outline size small on click start recording microphone 3.5 and record now that this component is finished we can go back inside of voice create form and we can go ahead and find our tabs so let's scroll down here until we find the actual voice create form and in here we should find the tabs perfect and for we currently don't have tabs content at all for record so let's create it tabs content with value record just beneath the tabs content for upload and import import the voice recorder like this dot slash voice recorder pass along the file on file change and is invalid let's go ahead and see that now when I click on record, which is disabled. So I have to enable it, remove the disabled attribute here. And there we go. Let's go ahead and allow the microphone. So allow. And you can see that as I speak, I get a waveform here. So I can exactly see if my audio is being captured or not. And I can also see the seconds here. When I click this, I can go ahead and listen to my voice. You can see the seconds repeat here as well and now I'm going to try and upload this and just like that voice was created successfully and here it is my custom Antonio cloned voice I can now use this voice and I can tell a silly joke in my voice now let's go ahead and wrap it up with an ability to delete custom voices because currently we can only listen to them but we cannot delete them so for that we have to go back inside of the voice card component, which is located in source features components voice card. And in here, let's go ahead and let's add the following imports toast from sonor use mutation and use query client. And then let's also go ahead and add alert action cancel content description footer header and title. We already have drop down. We are missing use trpc. So let's add that here at the bottom use trpc from trpc client perfect now let's go ahead and let's create some mutations here so once we define audio source once we have this let's add use trpc use query client let's go ahead and create a delete mutation here and in here we're simply going to call trpc voices delete mutation options on success toast voice deleted successfully and invalidate trpc voices get all query and on error simply toast error fail to delete voice we already have trpc voices dot delete organization procedure which allows us to only delete custom voices for that organization that the user is currently logged in so we already have that implemented perfect and now we have to add the ability to for this action to appear in the drop down menu because right now we only have one drop down menu item so what we're going to do here is we're going to go ahead and add another drop down menu item and we're going to import trash to icon from lucid react it's going to have the exact same class name except text destructive so not the exact same sorry text destructive and focus text destructive trash to icon and span delete voice and this will also be text destructive now we also have to maintain a state for this set show delete dialogue so let's go ahead and add that here so set show delete dialogue use state so import use state from react right here. And now let's go ahead down and actually use it here. So after a drop down menu, we're going to go ahead and check if voice variant is custom once again. And only then we're going to go ahead and render an alert dialog for them with open show delete the dialog and on open change set show delete the dialog. In here, we're going to go ahead and do the usual alert alert dialog composition So that going to be alert dialog content header title some description with the voice name we are trying to delete And then once we close the header we going to go ahead and open the alert dialog footer Inside of the alert dialog footer, we're going to go ahead and add a cancel button, which will be disabled if the mutation is already pending. And then we're going to add the alert dialog action. The alert dialog action will have a variant of destructive. it's also going to be disabled if the delete mutation is pending and on click it will prevent default and call the delete mutation with the property of id the voice id of this card on success it's going to close this dialogue and it's going to show a different text depending on what's currently happening let's try it out so you can see i have delete voice on my custom team voices but i don't have it for built-in voices so i'm going to try and delete this old one here delete voice, delete, and let's wait for a second. There we go. Voice is deleted. Amazing, amazing job. You just finished the largest chapter of this entire project. You can now upload, you can record, you can delete. You built the entire voice management. We have cache invalidation, live form visualization, in-browser audio recording, drag and drop file upload, and we can search through voices. We implemented so, so much in this chapter. Now it's time to commit it. Chapter 8, voice management. As always, let's go ahead and just make sure everything is fine. So npm run build, npm run lint to make sure there are no errors. And then we're going to go ahead and commit. Great. So both of my functions have passed. So now I'm going to git checkout b08. And this is a voice management. management like that, git add, git commit 08 voice management. And then git push u dash u origin 08 voice management. Perfect. Then let's go ahead and open a pull request and let's merge it. Brilliant. So let's open this pull request. I'm going to go ahead and immediately merge it. We know that CICD is going to work because we just checked the npm run lint. And once we've done that, we can go ahead and do git checkout main, git pool origin main to make sure we are up to date with what we've just merged. And beautiful. I think this is just a temporary TypeScript error. There we go. It resolved itself. We are now on the main branch. and here I can see voice management being merged into the main branch. Brilliant. So in a few seconds, you should also see all of these changes on your railway production instance. So go ahead and keep track of it here. You can see it's currently deploying. And once it deploys, you will be able to try voice recording on your live instance. Amazing, amazing job. And see you in the next chapter. In this chapter, we're going to add subscription billing to monetize the app. We will integrate pay-as-you-go subscriptions, which will include checkout, customer portal, usage metering, and subscription gates that are going to block text-to-speech generations and voice creations for non-subscribers. By the end, the app will be fully monetized with a sidebar usage or upgrade card. So this is a brief diagram of what that's going to look like. Before the user is subscribed, they're going to see an upgrade card. After they subscribe, they're going to see a usage card. If a user is not subscribed and attempts to generate a text-to-speech, they will be blocked with a toast saying subscription required. We're going to go ahead and learn the following key concepts. You will learn how to do subscription gating for any premium features you want to do. You will learn how to ingest metered events so that you can exactly bill users depending on how much they spend or how much costs they have made you. You will learn how to add actionable toasts and how to develop the billing TRPC router. The build order will start with the SAID router combined with Polar SDK, which I'm going to talk about more in a second, followed by subscription gates, toasts and usage metering, and sidebar usage guard. So at the moment, we don't have anything in our sidebar. and if I click generate speech, I will be able to do that. So let's go ahead and change that so that only subscribed users can do that and so that this estimation actually bills the user that much. In order to do that, we're going to add Polar. So why Polar? Well, let's look at an example. How would we do this with Stripe, which is, no questions asked, the industry standard? Well, I worked a lot with Stripe. In fact, most of my tutorials used Stripe. And I already know at the top of my head what we'd have to do. We'd start with building the webhook endpoints. After that, we would need to do a database table for customer and subscription state. Then we would need to do a migration to push that to the database. Then we would have to add a separate metering solution. This will include either bringing in a third-party API, or if we want to build our own, we would have to be tracking every single text-to-speech generation, we would have to aggregate usage per billing period, and then we would have to report it to Stripe's Meter API. That alone can take almost two chapters to complete. Polar lets us skip all of that. You can see their very bold line here. Turn your software into a business with six lines of code. And they're not joking when they say that. In fact, the entire billing integration will fit into one TRPC router and then a few lines of client code. So I'm absolutely blown away by Polar. They are truly beating Stripe simply because of how much they focus on developer experience. And you are a developer, so you will definitely appreciate all the work they've done in that aspect. On the business side, they are also a global merchant of record, meaning that they handle all the tax for you. And as much as I know, they are the cheapest merchant of record on the market. That is currently not important for our development state. So let's just go ahead and create an account. Once you create an account, you will either see an existing project if you have one, or you will most likely have to create a new organization, right? So let me go ahead and slow down. if you have an existing project you go ahead and click down here and click on new organization if you didn't have an existing project this will be the screen you see in here i'm going to go ahead and call this resonance production and yes make sure you name this one production choose your default payment currency go ahead and click that you understand the restrictions above and click continue. What we've done now is we have created, let's skip onboarding for now. What we've done now is we have created an organization for production. But in order to do it for development, you have to click on go to sandbox. And in here, you will most likely have to log in again, if you didn't, right? So you can see that sandbox is its own site. So when I go ahead then log in, I am back in this dashboard, but on a completely different site. And all projects that exist here are different. So again, if you don't have any project, you will most likely be prompted to create one. So in this one, I'm going to add resonance development like this, and I'm going to use the same default payment currency. And now I have both an organization for production and also an organization for development. So now I'm going to skip onboarding and I'm going to go ahead and set up everything I need here to create the metered product. So I'm going to go and click on products here and I'm going to go inside of meters. I'm going to create my first meter here. So for the name of this meter, I'm going to name it text to speech characters and I'm going to zoom in even more a bit all right for the filters i'm gonna go ahead and choose that name equals and then i'm gonna write tts underscore generation and i'm going to select it there we go text to speech underscore generation and for the aggregation i'm going to select some over characters like this characters and i'm going to go ahead and click preview here and i'm going to just zoom out and i will confirm once more everything is okay and i will click create meter right here so here we have it text to speech characters so some aggregation using characters so make sure you have the exact same as me here. And in here, you can already see how you can use it using polar SDK. So you will use the name text to speech generation, you will pass in the external customer ID, which in our case will be the organization ID. And then we're just going to go ahead and pass the characters or whatever else we want to do. Great. So that is one thing we have to do. Now let's go ahead and create another meter. And this one will be called voice creation. Now this one will be a little bit simpler. So in the condition group here, name equals voice underscore creation. That's the only thing we care about. And the aggregation will be simpler. We're simply going to count the number of event occurrences. All right. And then I'm going to click create meter. so you should now have voice creation which is a count aggregation and you should have text to speech characters which are a sum aggregation over characters perfect we are now ready to create an actual product that is going to use those meters so let's go ahead and click new product here i'm going to call this resonance pro you can of course do it whatever you prefer and this will be a recurring subscription coming in every month. Then let's go ahead and I will leave the US dollars at my currency here. And what I'm going to do is the following. I'm going to add a metered price and I'm going to select text to speech characters. And per unit, meaning per character, I'm going to choose 0 If you want you can add a limit so your users can never spend more than a certain amount or you can leave it as empty Now this of course is what I somehow would feel is reasonable for text-to-speech characters. Obviously, I might be wrong, but the cool thing is you can calculate how much self-hosting and model inference on model is costing you, and then you can simply adjust the amount per unit based on that. That's the great thing about this. You can exactly cover all of the costs that you have on this website. Then you can add an additional price. And in here, we're going to go ahead and add another metered price, voice creation. And in here, you can do whatever you want. So voice creation is almost free for you, simply because Cloudflare's R2 storage is so cheap. And there's nothing special to train the voice, right? Chatterbox just needs a voice sample. So there's no much training going on there. So you can almost choose whatever you want here. You can even make it free if you want to, but obviously you would have to start charging at some point. And these are the only ones I'm going to add. So the way this is going to work now, this will be a free subscription. So $0 a month, unless you start using any of these features. If you start creating voices, your monthly subscription will increase. If you start building generations, it will increase. And then in the next month, if you're not doing anything, it will be free once again. Now, if that's something you not like, you don't like, you can add an additional price, which can be a fixed price of, let's say, $20. I don't know, if you have another third-party app and you want to cover the costs of that app, you can do that. So it will be $20 baseline, and then it will be an additional $0.25 for every voice you have created, and additional $0.30 for a thousand characters. So whatever you want. For example, just to make it easier to demonstrate, I'm not going to add any fixed price. You can always add it later. So for now, just focus on the metered price of voice creation and text-to-speech characters. Perfect. Let's go ahead now. And if you want, you can also add a description. Where is it? Checkout page right here. In the description, it might be a good idea to just explain to your users what is in here. Pay as you go text to speech generation and voice cloning. Convert text to natural sounding audio using a variety of voices and models. It will build at 30 cents per thousand characters, create custom voice clones at 25 cents per clone. So something so that they understand without doing the calculation in their head. And let's go ahead and hit create product. Great. We now have that. And now let's go ahead and go ahead and create the developer token here. So go inside of settings, general, developers, and let's hit create token. I'm going to select all of these, and this will be called app development. And I'm going to go ahead and make it never expire so it's easier to develop with it. I'm going to create organization access token and I'm going to go ahead and copy it. Now that I have that I'm going to go inside of my dot environment file right here and at the top of my file, whoops I deleted sentry, I'm going to add polar access token. Beneath it I'm going to add polar server and I'm going to change it to sandbox here. And then I'm going to add Polar product ID. So for the product ID, I'm going to go back to my products and I'm going to use this right here to copy the product ID. You can also do it from within here, I think. Copy product ID. Okay. And then just go ahead and add that as well. Great. Now let's go ahead and let's add Polar's SDK, which you can find the documentation for right here. So let me go ahead and open this. Let me find the introduction. And if you want, you can even ask their AI. But right now, what I want to do is I just want to go ahead and install the most basic Polar SDK. So they are telling you to install Next.js here. And this would usually be true. But we're going to specifically be using this with trpc. So because of that, we just needed in general TypeScript. So let's do this instead. npm install polar shsdk. Their pure Next.js developer experience is absolutely mind-blowing. But because we are using trpc, we're going to use just this normal one, which is equally as good. You don't have to worry about that. Great. So I'm going to show you the version just so you are aware as usual. Package.json, not lock. package.json polar here it is 045.0 and now let's go ahead and develop the billing router so we just added that package here and what i would suggest before we develop the billing router is that we go inside of our source lib environment file and let's just add those new environments, polar access token. Then let's go ahead and add a polar server, which can either be sandbox or production with default being sandbox. And let's do polar product ID, which is also required. So we must have these three. If you have named them incorrectly, or if they're missing, your app is going to break. So you know that you're missing something. All right. So now I'm going to create an instance of Polar inside of this lib folder. So polar.ts. And in here, I'm going to import Polar from our new package, and then I'm going to add the environment file. So we simply initialize new Polar, Polar access token, and Polar server. Then let's go ahead and let's create the billing router. So I'm going to go ahead inside of trpc, routers, billing.ts. In here, I'm going to go ahead and import trpc error from the package, polar from our newly created lib, environment from the lib, and create trpc router and organization procedure. Then I'm going to go ahead and export const billing router to be create trpc router. And what I'm going to do is I'm going to build the create checkout procedure. The create checkout procedure will be an organization procedure and it will use Polar Checkouts Create with what products? Our singular product ID. And to the external customer ID, it's going to map the currently logged in's user organization ID. So this product will be purchased on an organization level. So everyone in an organization will automatically be able to use the premium subscription. So this is super important. and this forward thinking with their SDK allows us to avoid webhooks, to avoid customer tables, to avoid subscription tables, and so many other things. So this makes our life so much easier. If you were building a single tenant app, this could have been a user ID. But since we are building a multi-tenant app, this is an organization ID. If the result doesn't return the URL, it means it failed to create a checkout session. Otherwise, let's go ahead and pass in the checkout URL here. I believe you can also add a success URL here, which can be process.environment app URL, I believe. If we have it inside of our .environment, we do. So just basically redirecting us to the root page. Beautiful. Now, the next thing we have to do is the ability for a user to manage their subscription. We can do that using the customer portal. So let's create a procedure, create portal session. Again, an organization procedure. And you can see how simple it is now. We use polar, customer sessions.create, and simply passing the external customer ID. And that is enough to create the customer portal URL so we can redirect the user and they can see the entire organization's usage. That's it. That's all the code needed. You can see how beautifully simple this is because of this one ingenious field. And then let's go ahead and implement a get status procedure. Get status procedure will tell us whether the user is subscribed or not. And once again, we can do that with the external ID. So let's go ahead and open a try and catch. inside of try here let's get the customer state using await polar customers get state external and pass in the external id once again to be the organization's id in here we're going to go ahead and check if the organization id has active subscriptions so if customer states has active subscriptions or if it doesn't we're going to fall back to an empty array and we check if the length is above zero. And then let's go ahead and do a calculation of estimated costs that they have. So we're going to start with zero and then we're going to go ahead and open for each subscription of customer's active subscription for each meter of that certain subscriptions meters. let's increase the estimated costs by the current meter amount. This is simply forward thinking from us so that if you decide to enable multiple subscriptions because Polar supports that in the future it will still work just fine. All right and let's return has active subscription, customer ID and estimated cost in cents. And inside of the catch function, we're simply going to do customer doesn't exist yet in Polar. So let's just return all falsy or nullish elements. Now let's go ahead and let's build the usage container with this API information. So I'm going to go ahead inside of features and I'm going to create a new folder called billing. inside of here I'm going to add components and then inside I'm going to add usage container.tsx I going to go ahead and add the imports use callback use query use mutation button spinner a missing use checkout hook which we going to create and useTRPC So I forgot about the useCheckout hook so let quickly create it We going to go ahead inside of billing, and let's go ahead and import and add hooks, and let's add useCheckout.ds. So in here, we're going to add useCallback, useMutation, and useTRPC. We're going to export function use checkout we're going to add trpc then beneath it we're going to add a mutation to call create checkout and one thing i've noticed right now is that we forgot to map our billing router to trpc so to fix this we have to go inside of trpc routers underscore app and let's add billing, billing router. And now we can import our newly created billing router here. Let's go back to use checkout where now we no longer have any errors. And what's left is to simply create a checkout callback. The checkout callback will call mutation.mutate. It will pass undefined in the first argument and on success, it will load the checkout URL and replace the current window location href with it. It will have mutation in the dependency array. This hook has to return the following properties. Checkout is pending mapped to mutation is pending. Now that we have this hook, we can go back inside of the usage container. And I'm going to go ahead and modify this to be features billing hooks. Just like that. Now let's go ahead and add a simple format currency function, which accepts cents simply because our billing returns in cents and let's use entl api to transform this into us dollars so they are cents so we divide them by a hundred here now let's go ahead and define a function upgrade card so this is for users who are not yet upgraded so i'm going to go ahead and add use checkout and get the checkout function and is pending then let's go ahead and add a return method here this will be the jsx let's go ahead and add a div then let's go ahead and add some information so another div which encapsulates two paragraphs first one saying pay as you go which is kind of a title and the second one with an extra small text and a bit muted will say generate speech starting at 30 cents per thousand characters and then what we need to do outside of this div is we have to render a button with variant of outline class name full text extra small size small disabled if the checkout is pending and on click checkout and then inside of it we're going to check if the checkout is spending and then show a spinner with redirecting otherwise show upgrade now let's go ahead and build another component called usage card. So this usage card will have a property estimated cost in cents, because that's what the API returns. And this will be used to open the customer portal. Let's add trpc and the portal mutation using trpc billing create portal session and passing the mutation options. Let's go ahead and add an open portal callback, which will call the mutation pass in undefined in the first argument on success get the portal url and open it in a blank new tab so the behavior is slightly different from the checkout one and in here let's go ahead and return the following we're going to add another div it will be a very similar example here so another div which will hold three paragraphs this time the first one is the title current usage The second one will format currency and estimated cost in cents. And the last one will simply tell the user that this is the estimated cost for this period. And then beneath that, we're going to render a button with variant outline, class name, size small, disabled. If portal mutation is pending, on click, it will open portal and do a very simple if else spinner. If it's redirecting, otherwise it will say manage subscription. Great. And then the final component here is the usage container, which has the trpc, the data so that we know if we have active subscriptions, so we know when to render a usage card and when to render the upgrade card. And that is encapsulated within a div, which has group data collapsible icon hidden. So this is hidden on mobile and BG background border, border and rounded large and padding three. Great. Now we can go ahead and render this in the sidebar. Let's go inside of the dashboard sidebar component and let's go ahead and find the sidebar footer here and I'm going to add it right where it starts. Here it is, sidebar footer, and I'm going to add usage container here. So make sure you import usage container from features billing components usage container and then go ahead onto your app here and you should now see an option to upgrade and when you go ahead and click upgrade you will be redirected you can see right here so the subscription is free but the text to speech characters cost this much and voice creation costs this much so you can go ahead and now fill in the data you can use a stripes success card 42424242 till the end any expiration in the future any security code any name any country here go ahead and click submit payment and after a while you will see that we are redirected back and now we see the current usage and now we can open manage subscription which will basically tell us how much money we spent this month you can see it's active it's free but it will not be free if we start using the metered charges. And what's cool inside of your app right here, if you go inside of your customers here and find your customer, scroll down here and you will find their external ID to be an organization ID. So they have purchased on behalf of an organization. And now we can easily meter John's organization. Now let's add subscription gates to block the users from generating something if they don't have an active subscription. We can do that quite easily by creating a new organization and let's call this one free organization. So since this is organization based, once you refresh, you will see that in here it still says pay as you go. Only once you go into your old one and refresh will you see the current usage. I will look into if Organization Switcher has a way to refresh for you so you don't have to do it. Just make sure you are in free one for now and let's go on to the text to speech page. In order to subscription gate this, we're gonna go ahead and find the generations router. Then I'm gonna go ahead and add a polar import. Then I'm gonna find the create procedure. And inside of here, before I start generating anything, I'm going to go ahead and check for active subscriptions before generations. So let me go ahead and show you how that is going to look like. So it's this code right here. Check for active subscriptions. We open a try and catch block. And inside of the try block, we attempt to get the customer state using await all our customers get state external with the current organization ID. In here, we check if the user has an active subscription using customer has active subscriptions or fall back to an empty array. If there is not a single active subscription, we throw a new TRPC error with code forbidden and message subscription required. This is extremely important. It needs to be exactly like this. Or if there is any other error, we simply cannot verify that this user has a subscription. So we throw another TRPC error, subscription required. So that's the entire check that we need. I'm going to zoom out for a second so you can see the entire code in one screen. So it's this implementation right here. Now, let's go ahead and go all the way down. Before we return the generation ID, we have to meter this usage. So I'm going to go ahead and add the following. We're going to ingest usage event to Polar. So polar.events.ingest. Go ahead and open an object. and the events we're looking for is name TTS underscore generation. This needs to match what you have defined inside of your products and what you have created inside of your meters here. So it needs to be called TTS generation right here. You need to pass the external customer ID and the metadata will be characters, which is input.text.length. So these characters are what we are summing against. So this needs to be valid to make sure you're passing the characters properly. So this is the entire ingestion code right here. We do it just before we return. The code is identical for voices create route. Let's go in here. So instead of source API, app folder API, voices create route.ts, let's go ahead and import polar from lib polar. and then again we have to check for an active subscription before voice creation so let's go ahead and do the authorization first because we need an organization id and then let's go ahead and simply check for active subscriptions before voice generation we do the exact same thing we get the customer state thanks to the external organization id we check for active subscriptions If none, make sure you throw error subscription underscore required. This is very important. And in the catch, we do the same thing. And then after everything is successful, we need to make an ingest. So let's scroll down. After a voice has been successfully created, before we return the successful response, let's go ahead and make sure we ingest usage event to Polar. In this one, we are looking for voice underscore creation. So this is the other meter that we have, voice creation. So in here, it's very important that you have named this condition group equals voice underscore creation, because that's what we're looking at here. Metadata isn't important here because we count for each event. And the only thing that's important, as always,\nalways is the external customer ID. And just like that, we have added usage-based billing in our app. Now, the only problem is, right now, our app will not show a proper subscribe error. So if I try writing something in my free organization here, it will fail with subscription-required toast. So let's go ahead and capture that and open a proper redirect. So inside of text-to-speech form element, which is located in source features text-to-speech components text-to-speech form, I'm going to go ahead and I'm going to add use checkout, which we can import from features, billing, hooks, use checkout. And once we have the checkout here, all that's left is to check what type of error message is being thrown. In here, we almost have that prepared. In catch error inside of onSubmit function, let's remove toast error. And instead, let's check if message is subscription required, open a toast with onclick for checkout otherwise do a message let's try this again so hello world and if i click generate speech it says subscription required and once i click here it redirects me to upgrade now let's do the same for the voice create dialogue voice create dialogue component and in here let go ahead and add use checkout hook So make sure you add this Add use callback Add toast from Sonor And very simply, if handle error receives a message with subscription required, open a toast with an action to open checkout. Otherwise, simply open a toast with error. And now what you can do is you can append this handle error function to all instances of voice create form like this. and this one down here. And now when you try to create a voice, you will get a very similar example. So if I go ahead right here and attempt to create a voice, I will get an error. One thing I forgot to do is enable voice cloning to open this custom voice dialogue. Let's go ahead and quickly do that. Go inside of dashboard sidebar component. Let's go ahead and scroll down to the dashboard sidebar here. And right before we render the sidebar, I'm going to go ahead and open a fragment and I'm going to render the voice create dialogue. I'm going to go ahead and close the fragment down here where the entire sidebar ends. I'm going to indent the entire thing here like this and this. All right. I'm going to import voice create dialogue from features, voices, components, voice create dialogue. Then I'm I gonna go ahead and add to the dashboard sidebar function a state So I need to import this from React as well And then very simply what I going to do is I going to find voice cloning here and I going to add on click set voice cloning to true and that will open the voice create dialog. So when you click this it's going to open. Beautiful. Keep in mind that when testing ingestion it might not immediately upgrade. Right. So sometimes you might need to leave it like five to ten minutes before the actual polar does the ingestion and calculation. If you're wondering if it works or not, simply go inside of your meters, click for text-to-speech characters, and click on events. If you can see events here, everything works perfectly fine. You can see it's also related to a certain user, so if you can see your user, even if it says zero, it is still working as intended. Beautiful. You have completed the entire project all that's left is to redeploy so i'm going to go ahead and do the following i'm going to go ahead and do npm run build then npm run lint just to confirm both of this work then i'm going to check out the billing i'm going to add everything i'm going to commit so message 09 billing and git push u origin 09 billing and now as usual let's go ahead and open a pull request and merge. Once you opened your pull request go ahead and merge it and that officially marks the last merge we are going to do here Beautiful We have a beautiful history here if you ever want to go back to see what we did And let's go ahead and get checkout back to main and get pool origin main once again. So we are up to date on our main branch. And for the deployment, we have to open our environment file once again. And we have to add all of these new polar things we have added here. So let's go to railway here. Let's go inside of dashboard and make sure you are in production and go ahead and click on variables here. Actually, open your production instance somewhere. So make sure you have the URL. Go inside of variables, click raw editor and go ahead and replace all of them except skip environment validation. So I'm going to paste all of them and then copy the URL that you have for production. You can see the URL right here. right copy it and find app url and make sure that you change it i almost forget that and remove the trailing slash at the end click update variables click deploy and there we go just like that your app now has polar in it as well later you will change from sandbox to production and your app will be ready amazing amazing job thank you so much for following along with this tutorial you've completed the last chapter and see you in the next tutorial remember to leave a like and subscribe if you like this type of content. Thank you for watching.",
  "transcript_chars": 551588,
  "transcript_filled_at": "2026-06-06T16:15:37.848754+00:00",
  "transcript_filled_by": "tk-bulk-groq-retry-20260606"
}