In this chapter we're going to implement dashboard chat giving the ability to our operators to intercept the chat as humans and write messages to the user. We're also going to do some things from the previous chapter that we noticed like the sidebar state not working And also one thing that I know we forgot, which is Yotai provider. So let's go ahead and let's first fix the invalid sidebar default state, which is actually a very funny bug. So if you do turbo dev and go inside of your web app, so localhost 3000, you will notice that this sidebar is not exactly behaving as it should. Basically it seems like it's always collapsed, Almost as if we added a variable that it's collapsed.
Because right now when I expand it, when I click here in its border here, now it's expanded. Great. And what I expect to happen is that the cookie changes and that when I refresh here, it should stay expanded. But that's not the case. And I actually figured out what it is and it's quite funny if you go inside of web and then go inside of modules dashboard ui layouts dashboard layout The first thing I thought was maybe this cannot be an asynchronous component because I know that layout which is the reserved file name in the dashboard route group this can be asynchronous so maybe I thought oh maybe this needs to be asynchronous and then this can be asynchronous but that's not the case.
This doesn't have to be asynchronous. This is just fine. The problem is literally in the constant. Yes, you can see that the sidebar cookie name is sidebar state and We thought that it was a good idea to export this so we don't misspell it accidentally. But try and replacing this with a string instead.
So the exact same thing from that constant and refresh. And now it works. Try and collapse it and refresh and it will stay closed. Expand it and refresh and it will stay open. So the bug is quite funny.
Looks like there is something weird happening in runtime, in SSR because of the TurboRepo, Monorepo structure because we are technically importing this constant from a package of ours. And maybe it's not quick enough by the time server-side rendering fires, because this is exclusively for server-side rendering needed, right? So I'm not too smart to tell you why this is happening, but I'm 99% sure that this would not happen in production when we actually build the app. I'm 99% sure this is development only bug. So yeah, not so super fun but at least we know it's very easy to fix.
Let's just bring this back to this and let's add a little comment Using sidebar cookie name from sidebar component does not work due to Monorepo and SSR. Let's go ahead and do that and remove the sidebar cookie name. This way we at least know why we have to add string like this. Great. Now let's talk about the other thing that I wrote here.
Add Yotai provider. So what is this? In the previous chapter, instead of our conversations panel component, We have this select component which uses the status filter atom. And we just added Yotai for the first time instead of our web app because we use the Yotai instead of Widget. But take a look at how Widget works.
Instead of components, providers, we have this provider from Yotai. But in here, in the web app, we never added that provider. So why does Yotai work? Well, looking at Yotai's documentation for Next.js, we have the explanation. By default, Yotai uses implicit global store to keep track of atom values.
This is what is referred to as providerless mode. This becomes an issue in SSR scenario because this global store is kept alive and is shared between multiple requests which can lead to bugs and security risks. So that's why it is preferred to import a provider from Yotai. Now as far as I know we're not going to be using Yotai anywhere outside of the dashboard. So what we can do is we can go inside of our dashboard route group and let's go inside of layout and let's open the dashboard layout here and let's simply add the provider here in Yotai React.
And we can actually add it even further. Let me just do this. We can add it like here. Yeah, inside the organization guard. I do want them to be above the provider from Yotai.
So like this, auth guard, organization guard, and then Yotai provider. And let me just check inside of my widget layout, my apologies, providers. I import provider from Yotai. So I'm going to import from Yotai here as well, not from the Yotai React. And I don't think anything should work any differently now.
We are just no longer using the providerless mode. Right, so now when I refresh, this should still stay escalated. Exactly. So we make it easier for our users so they don't have to constantly switch within the one they prefer. Awesome.
Now let's go ahead and let's develop the chat. So the first thing I want to do is I actually want to resolve this because we have this weird conversations page that isn't actually displaying anything and it's super easy for us to add something here. So let's go inside of apps, web, modules. Let's go inside of dashboard, UI and let's go ahead and create views. And then of here let's go ahead and return a div with a class name flex, flex1, items center, justify center and gap x2.
Let's render the image component with an Alt of Logo, Height of 40, Width of 40, source, logo.svg, and let's add a paragraph echo. Class name, font semi-bold, text large. Like this. Now that we have this conversations view, make sure to save it. And then go inside of your Apps, Web App, Dashboard, Conversations, page.tsx.
And in here, you can just do Conversations view. As simple as that, because That's the only thing this page will display. I'll explain why in a second. So when we don't have any conversation selected, we don't really have anything to display here. So why don't we just display this kind of placeholder text?
You can of course choose if you want to display a state like select a message or start a conversation, something like that. Only when we click on one of these, are we going to render a different page. So let's go ahead and do that now. So inside of conversations, let's create a new folder, conversation ID. The capitalization here matters.
So be mindful of that and add page.tsx inside. And let's go ahead and return the page, div conversation ID, Like this. And now when you click here, you should no longer be having any errors. Instead, you should see this now has been replaced with conversation ID. Why does this work?
And what if yours doesn't? If yours doesn't work, head back to Modules, Dashboard, UI, Layouts, and let's go inside of Conversations layout and head into the Conversations panel. So in here, you have to find where you iterate over your items here. So conversationresults.map and take a look at the link component. It should lead to forward slash conversations and then your conversation id.
So that's exactly what we just created. Let me close things. Instead of app dashboard we have conversations and then we just created the conversations ID which can be anything. This is how you create a dynamic params instead of Next.js. So that's why it should work for you.
Now, why did I say that the capitalization matters? The reason it matters is because of this. If you want to extract the current ID you're going to have to add the following params which are a type of promise and then inside conversation ID which is a type of string and then in here you can destructure the params. So you can see how I spelled conversation ID with capital I. It is because of how we named it in the folder.
If it was named something else in this folder, you would also have to extract it like that from here. That's what I say conversation capitalization matters. Unfortunately, this is not strictly typed yet. I think that maybe you can do it with some experimental Next.js features, but right now you can type anything here and it won't throw you a warning so you have to manually confirm that you did this correctly and then you can mark this as an asynchronous page and then you can destructure the conversation id by using await params and then render the conversation id and there we go Now we can see that each of your selected conversations, which by the way looks super cool when selected, also displays the proper conversation ID. And now I think you already know the flow of what we have to do.
We have to pass this conversation ID to our newly created conversation ID view, which we're going to create soon. And then we're going to have to fetch the convex API. So how about we do the complex part first and develop inside of packages backend convex private conversations.ts. Let's go just above our getMany here and let's export const getOne. And inside of here let's go ahead and define the arguments to accept conversation ID, which is a VID of conversations.
And that's the only thing we need actually. And then let's go ahead and define the handler. And let's go ahead and verify that we are logged in and that we are within the proper organization. For that, we can literally copy the things from above, from below, my apologies, like this. Let me just invent this properly.
So using context.auth, we can get the identity. If the identity is missing we are not logged in. Then we can extract organization id and then we can throw a new convex error here. Now let's go ahead and let's get the conversation by using await context database get conversation ID using arguments.conversation ID. And now if there is no conversation, Let's go ahead and let's throw new convex error, code not found, and a message conversation not found.
Now let's check if conversation organization ID is different from the one our user is currently logged in with. In that case, we can also throw back a convex error and we can pass the code unauthorized with the message invalid organization ID. Let's do it like this, like that. Great. And now let's go ahead And let's add the contact session relating for this conversation.
So very similarly, contact session is a weight context database gate from our now proper at this point, you know, completely valid conversation object dot contact session ID. And if there is no contact session ID, let's also throw an error here. So not found contact session not found. Finally, let's spread the conversation and let's append contact session. Object.
Not ID. Contact session. There we go. So now we are safely loading conversation only for this organization ID and we are only returning it if we found a matching contact session so that we know the user's info. Excellent.
Now that we have this, let's go ahead and let's display the conversation view component. So let's go ahead and close our back-end package. Let's go inside of apps, web, app, dashboard, conversations, conversation ID page, and inside of here, let's go ahead and let's return conversation view. My apologies, conversation ID view, which we don't yet have, and pass in the conversation ID, conversation ID as ID, using workspace backend generated data model, Conversations. Now let's develop the conversation ID view.
So this will be located inside of modules dashboard UI views. Conversation ID view dot TSX. Let's mark it as use client and let's export const conversation ID view like this. Let's go ahead and destruct conversation ID from here and let's go ahead and type that conversation ID. It's going to be a type of ID conversations.
Just like this. And let's go ahead and let's return a div client view because this is a client component. Now go back here and you can now safely import conversation ID view and your types should be working. Perfect. And there we go.
Client view. Excellent. So now let's go ahead and let's add our const conversation use query from convex react here. And let's pass in the API private conversations, get one and pass in the conversation ID like this. And now in here, Let's go ahead and do JSON stringify conversation.
And now for each of your conversations here, you should be able to see, looks kind of weird, but basically here it is your JSON object of that. Great. And now let's actually develop it so it looks like something. So I'm going to go ahead and give this div a class name here. Flex, full height, flex column, bg muted.
Inside of here I'm going to create a little header. So let's give this a class name FlexItemsCenter, justify between border bottom, BG background, padding 2.5. Inside of here I'm going to add a button component, so make sure you have imported it. And I will simply add more horizontal icon from Lucid React. Like this.
Let's go ahead and give this a size of small and a variant of ghost. And let's see how that looks like. There we go. So it should be exactly the same height as our neighboring element. That's why I specified this specific padding here because I believe inside of the conversations panel, I'm using the exact same thing here.
Or at least it ends up being the same thing when combined with this div and select inside of it. Maybe the smarter idea would be to literally write the height value, but I managed to do it with padding, so I'm going to keep doing it with padding. And let's just keep this component like this for now. It's not going to do anything smart. Now what I want to do is I want to import all of our AI related components, which we added in some previous chapter when we were developing the widget chat.
So let's start by adding AI conversation, content and scroll button from workspace UI components AI conversation. These are the ones we added manually and then we had to install the packages if you remember. So make sure you have this from AI conversation. Now from AI input, add the input button, submit, text area, toolbar, and tools. And now let's add from AI message, AI message and AI message content and now let's go ahead and let's add AI response from AI response Now let's add all the form fields.
So form and form field from workspace UI components form. Let's go ahead and let's add Zod. Let's get them all together here. So Zod and let's do a React hook form. It looks like we don't really have this here because we only use them inside of the widget.
Not sure if you remember and we had some problems with that. So hopefully this time it will be easier for us to do this but I think this is okay for now. Let's go ahead and let's try and install this. So save this file and let's take a look. So search for Zod and let's see.
So we have Zod 3.25.67 so I'm going to copy this because it's the working one and I'm going to do pnpm f web add zod at 325.67 instead of my web app right here so this should hopefully add the exact same version of Zod instead of Web. Perfect. Now let's do React Hook Form. So this is 761.1 with the caret. So bnbm fweb add react hook form at this version.
So now it should be the same thing. It should work just fine. Let's refresh. There we go. And we also have resolvers, Hook form resolvers 5.2.0.
So now let's do this as well. Pnpm fweb add. There we go, hook form resolvers and let me add this specific version. Hopefully everything will work out fine. If not, we might have to like delete node modules or something.
But yeah, all of our versions are now the same. So at least that is a good thing. And no more errors here. So let's continue developing. Let's define the form schema here just above the conversation ID view.
Form schema will simply be a message with a minimum requirement of one and message is required error, as simple as that. Now let's actually define our form here. So const form will be use form like this. Inside of here, add a resolver, which will be the Zod resolver and pass in form schema. Default values here are going to be message and just an empty string.
Now let's go ahead and let's add a type here z infer type of form schema And now let's go ahead and let's import the Zod resolver because I forgot to do that. There we go. So now that we have the Zod resolver set and all of this, let's go ahead and create a little submit method here. Const on submit will be an asynchronous method with values which are going to be the exact same type as this. And for now, let's just console log the values because I don't think we've created the create method just yet.
Perfect. And now we can go ahead and we can start developing this. So let me just close this. Whoops. Let me close this.
And just in case I'm going to do turbo dev just to convince myself that these packages didn't cause any problems because they were quite, they caused quite a bit of a problem in the widget component. But hopefully not now because we added correct versions. Anyway, let's go ahead now, outside of this div, which we can actually call header, so it's visually easier to separate them. Let's start the AI conversation component and let's go ahead and give it the maximum height which we can do because we know what is the height of this header. How do we know that?
Well remember in the conversations panel I did the same thing here so I know it's 53 pixels So we can actually copy this exact class name here and just add it here. But actually it won't be 53 pixels because we also have to account for the, let me show you, We also have to account for this part. So I already know the height of this. So altogether it will be 180 pixels. I just don't want you to think like I am where I'm pulling these numbers from.
Well, I know them in advance. So let's add AI conversation content. And in here, let's go ahead and let's we have to load the messages now. Yes. So let's see.
I think we're going to have to develop. I'm trying to think if we can reuse any of our existing messages, but I think the best way would be to create a new hook. All right, let's quickly fix this. Save this file, leave this error, close everything so it's simpler. Go inside of packages, backend, convex, and go into public and copy the messages and paste them inside of private here.
So now you can leave the create because we will technically need it, but let's focus on the get many now. So in here, we need thread ID and we don't need contact session anymore. Instead, what we need is our usual verification. So let's just copy this one and the organization ID thing like this. So replace the contact session in your newly copied private messages, get many, remove this, add this instead.
So we check if we are logged in and if we have the proper organization ID here. And now, instead of here, let's do some additional checks. So const conversation that we are trying to load the messages from will be fetched by my apologies, let's just query conversations. And let's just do with index by thread ID query query equals thread ID thread ID. And where Can we get the thread ID from...
Oh, arguments. I forgot that we have it here. Okay. Thread ID and unique because there can be only one combination. In case this one does not exist something is wrong.
So we should not be able to even load these messages in the first place. So let's throw back an error not found and let's say conversation not found. And now let's check if conversation Organization ID does not match the current organization ID that this user is logged into. And let's go ahead and throw in unauthorized invalid organization ID, like that. And then we can safely pass the paginated here.
Now I'm not sure if there is a way to pass in that user ID from here. If you remember when we, let me try and find conversations.ts in the public folder and create. So in here, when we create our first thread, we actually pass in the user ID here. So even if we didn't do this validation here, we should somehow technically be able to reuse that user ID, which as you know, we are having the organization ID for. Right, so we could just technically use this organization ID and then somehow pass it here, user ID like that.
And then it should only load that, but I have to explore the API. I'm not exactly sure if we can do that. I'm going to leave it like this for now because our validation is more than enough. We check if the organization matches, we check if the conversation exists, we check if the organization is present, all from this Auth user identity, which is not passed by the client, right? So we are more than secure when it comes to this.
So let's now go back inside of our conversations ID view, conversation ID view here. And now in here we're gonna have to add our messages. So let's do that just below the conversation. Const messages will be use thread messages. And you can import use thread messages from convex dev agent react.
Let me see, do we have this here? We don't have this. All right, so we need to import two UI messages and use thread messages from convex dev agent. As always, let's check what's the version we are on, this one. So I'm going to copy this and let's do pnpm fwebadd convex dev agent at this version.
And that should resolve the issue. There we go. So now, perfect works. Great. So now we have used thread messages here.
And let's go ahead and pass in api.private.messages, which we just added, .getMany. Now let's check, do we have the conversation from above? Let me just copy it like this, so conversation.threadId. If we do have it, let's pass in the thread ID here. Otherwise skip.
And let's add the initial number of items to be 10. And now we have our messages. Perfect. So now we can iterate over them. So let's go back inside of our AI conversation content here.
And let's go ahead and open two UI messages, which we imported from above, messages.results, or empty array, question mark.map, and then get the individual message. Let's return AI message component here. Let's go ahead and give it a from property here. So if message dot role is equal to user, In that case, we are the assistant. Otherwise, we are the user.
I know this is a little bit confusing, but let me add a comment to explain in reverse because we are watching from assistant perspective. So all messages that are coming from the user should be labeled as assistant because then they're going to be rendered in the opposite bubble. Let me show you here. So usually when you see a white bubble, it means that someone is responding to you. The assistant is responding to you.
But since we are the assistant In this case, we have to reverse that logic, right? I hope this will make more sense later on when we compare both the widget and the operator dashboard. So you'll see why we have to do this. And the key will be message ID. Now let's add AI message content.
AI response. Message content. Outside of the message content, check if message role is equal to user. And if it is at the dice bear avatar component which is a self-closing tag by the way. So dice bear avatar from workspace UI components dice bear avatar.
And let's go ahead and let's pass in the seed to be conversation contact session ID. And let me just see. So Yeah, yeah, this is fine. Seed size is 32. Yeah, okay.
Because technically this can be null. Okay, let's just for now fix it like this. User. All right. And then let's go ahead and add AI conversation scroll button like this.
Now let's see how this looks so far. Let's head to our localhost 3000. Let's select one of the messages here. Maybe refresh in case you've restarted your server in the meantime like I did so it has to rebuild. And there we go.
Looks very nice. And you can see how all the messages coming from the AI chatbot now look like we sent them because that's what we are. We are the assistant, right? So That should look like we are sending those messages. Great, I'm very happy with how this looks.
So the user's message seem like they are the assistant, but we are actually the assistant, right? It's confusing, I know, but it makes sense when you look at it because we are the operator, We are the dashboard. And now let's go ahead and let's develop outside of the AI conversation here, not paragraph. Let's add a div. Let's give it a class name of padding two and let's develop the form.
Let's go ahead and let's spread the form constant which we defined above. And then let's use AI input again because this is underneath a form element. That's why. Let's go ahead and give it on submit here. Form, handle submit, on submit method.
So you should have the on submit method which simply logs the values. And then in here, let's add form field which is a self-closing tag. And now we're just doing the chat CN form composition. So pass in the control form dot control. Disabled will be if conversation dot status is equal to resolve.
And we can just add a question mark here. Name will be message, render will be field, AI input text area, which is a self-closing tag. And now in here, let's just add some props here. So let's see, it will be disabled if conversation question mark status is resolved or if form form state is submitting or let's add a to do or if enhancing prompt, which we don't yet have, but we will have. On change here will be field.onChange.
On key down will very simply be the exact same thing that we have inside of our widget chat screen. So head inside of your apps widget module screens, widget chat screen, Find the form field, AI input text area on key down and just paste it here so you save yourself some time. Placeholder here, conversation, question mark status is equal to resolved. This conversation has been resolved. Otherwise type your response as an operator like this and value will be filled dot value.
All right, now inside of here, but still inside of the AI input, add AI input toolbar, AI input tools, and inside of here, AI input button. Now the button will have VAND2 icon from Lucid React and enhanced text. Just make sure you added the VAND2 icon and we already have AI input button imported here. And then outside of AI input tools, add AI input submit. I think it is a self-closing tag, yes, but we need to add some props here.
So it will be disabled if conversation status is resolved. Let's add a question mark here. Or, if not, form state is valid, so if the form state is invalid, if we haven't typed anything in yet, or if form state is submitting, and or if it's enhancing prompt, which we don't yet have. Status here will be ready, type will be submit. And now you should have a beautiful input here and the button to enhance the prompt which we don't yet have.
And in case this button is getting in the way you can try and hide it for this session like that. Now let's head back inside of our messages which we started developing. So backend convex private folder messages.ts. And we already have the create method here because we copied this from the public folder, right? But let's slightly modify the arguments.
So we are no longer going to need the contact session ID. And we are no longer going to need the thread ID. Instead, we are going to need the conversation ID, which is a type of ID conversations. Let's just fix this. There we go.
Conversation ID. And let's quickly copy the authorization checks from here. And yeah, you can definitely abstract this and create a method to reuse this. Convex even offers you something called convex helpers which allows you to create a role level security. But that's something I will explore for a different time.
For now I just want to focus on creating the project. So I'm going to remove this check for the contact session and I'm going to replace it with identity. Make sure you check for organization ID as well and then let's go ahead and let's check for the conversation here. And CSU, one thing we should probably change is this is a mutation. If you're wondering how come this is a mutation and the one from the public folder was an action?
Well, because in here, we are not actually going to be using the support agent at all. In here, we are responding as an operator. We are not generating an AI message. So in here, we can actually do this. Instead of using this internal query, we can remove this and just use the normal query.
And this will be arguments.conversationId. And then if the conversation is not available, not found. And then let's just quickly check one thing here, this basically, So we don't have to type it again. After we confirm that the conversation exists, confirm that we have the matching organization ID. Are we allowed to send messages in this conversation?
And then the same thing here for if the status is resolved, we shouldn't be able to send any messages. And then in here, let's go ahead and let's, instead of using AI to save a message, let's just save a human message. And you already know how to do this, believe it or not. Remember, instead of our conversations.ts in the public folder, when we allow the user to create a new conversation. We do the usual session check, blah blah blah, and then look at what we do.
We manually create a message. So we already know how to do this. Let's do it. I'm just going to copy this. I'm going to go back inside of my messages here so I'm working in the create method of the convex private messages.ds and paste it here.
Let's import save message from convex dev agent. Let's import components from generated API, remove the internal, we no longer need it. The thread id comes for conversation.threadId like this. And now in here let's add agent name and let's go ahead and pass this as identity.familyName. This doesn't really matter, but it could be a good idea for you to already keep track of which agent responded, right?
Because agent name is a property that exists within the message component itself. But we are not really differentiating between agents. We are kind of considering the entire organization an agent, even though it can have multiple operators, we never really showcase their image or their name. But internally, if you want to, you can keep the agent name here. You probably don't even need this field at all, but I'm just giving you an option to do that if you want to.
And for the role in here it will be assistant and the content will very simply be the prompt. And let's just see what's wrong with my prompt here. What is my prompt? Oh, my prompt doesn't exist. My prompt needs to be accessed through arguments.
There we go. Perfect. So now this is my save message method here. And yeah, let me just add a little to do check if agent name is needed or not. But for now, yeah, you can leave it as identity family name and then this will literally be the logged in user's name like Antonio or something like that.
But we are not displaying this anywhere. That's why I keep saying that it's not super important. Nor are we using it for literally any other thing. Now let's go ahead and use this create method. So let's go back instead of the individual conversation ID view here.
And let's go ahead and do const create message here. Use mutation from convex react API. Private messages create. Like this. Make sure you have added use mutation here and I think that we already have the API.
And now let's go ahead and let's add this. So what we now have to do here is open try and catch. Let's do a wait, create message. Passing the conversation ID and prompt will be values.message. And then do form reset.
And in the catch, let's go ahead and let's do toast, but we have to import toast from Sonar. Okay, for now, let's just do console error, error. Like this. And now that we have on submit, I think that that's all we have to do actually. So go ahead and just pick a random chat and try and respond.
This is a human response. There we go And you can see how no AI response is being triggered here because we have to trigger it through code, right? When we are looking at back-end convex private messages here, all we do is we save the message here. But for example, when we do messages.ts in the public folder, so click public here or just go inside of packages back and convex public folder messages. When we do create here it's a different thing, it's an action, we validate the user's session here and then what we do is we basically tell the agent what to respond with and we give it the prompt here.
So we call support agent all right generate some text back and if you're wondering well how does the user's message get saved then? Well, this convex component handles that for us. So when we call generate text, it will both save the message and it will respond the AI message. But in this case, we only want one of those things to happen. So we extract the save message from the convex agent component and we just save it still as an assistant because in this case yes we are the assistant in this case.
We are not the user. And let's just double check this you know by opening our localhost 3001 and let's just pick one with a working organization. You can see I have it saved in my URL here. So you know the deal already. Make sure you have a working organization ID in your widget or visit dashboardclerk.com and then in here click on the organizations tab.
Mine is loading slowly now but basically click on the organizations tab and then you can find the organization ID. So let's check things here. There we go. This is a human response. So if I go ahead and combine these two, you can see that, hello, I can respond and you can see how this looks like.
I Am a human. But still, if I say, hi there, I will still get an AI response. And you can see how it appears as if we sent that. That's exactly what we want. But what's actually supposed to happen is that when the operator responds, the AI should be turned off.
And we're going to handle this by creating a new status called unresolved. I mean, we already have that status, but we currently don't really have a way, my apologies, escalated status. That's the one I should be talking about. So when the operator responds, we're going to change this from unresolved to escalated. And then this means, okay, Either the user has requested a human or the operator chimed in themselves as a human.
So that's what we're going to focus the next chapters on. Tool calling and basically wrapping this up as much as possible, this operator dashboard. And maybe this enhance prompt as well, which is super simple. You will type something, click enhance, and it will give you a better response. And we also have the contact panel on the right here that we have to develop.
Perfect, but I think this was the goal of this chapter. Let's see, we added the Yotai provider, conversation ID view, messages functions and conversation functions and now let's merge this. So 18 dashboard chat. Let's go ahead and stage all of our changes here. 18 dashboard chat.
Let's commit them. I'm going to open a new branch. 18 dashboard chat and I will quickly publish the branch. And now let's go ahead And let's review our code just in case we have any serious security issues. We are actually quite on a good run.
In the last few pull requests, it was almost perfect. Let's see if luck follows us in this PR as well. And here we have the summary. We introduced a Conversations dashboard page displaying a list of conversations. We added a detailed Conversation View page allowing users to view and participate in conversation threads, send messages, and see role-based message styling.
We implemented form validation and input handling for sending messages within a conversation. We also added back-end support for fetching a single conversation, retrieving messages with pagination and creating new messages. Exactly what we did. In here, of course, we have the sequence diagram explaining what we do, but this is exactly the same thing as our widget screen chat. We are already familiar with this entire thing.
As for the comments, it suggests doing exactly what we have planned to do, adding the toast library here to display the proper error because we can't do that in the dashboard. In here I have a typo, it's unauthorized not unpolicyed. In here I am checking if I have a missing conversation and I'm throwing contact session not found, I'm pretty sure I'm doing the if check okay, but I'm throwing the incorrect message. So I think that I should actually keep doing the conversation here and just fix my convex message because in the private conversations there is no reason for us to check for the contact session. I think let me just quickly check messages actually conversations.ds in the private here.
Let's see line 49. I see. So we check the conversation here and then instead of using content accession, I'm using conversation here. So I never really check if this exists or not. Absolutely correct.
Thank you CodeRabbit. We're going to fix that in the next chapter. And in here, yeah, the same thing we mentioned. You can see how it knows from the types that agent name is optional. So we should either drop it or decide what to do with it.
Yeah, good comments here and one security issue fixed by CodeRabbit. Well, not exactly a security issue but proper inner join behavior fixed. So merge this, head inside of your main branch and click synchronize changes, click OK. And as always, double check that your graph looks properly. Let's click here.
There we go. I believe that marks the end of this chapter. Amazing job and see you in the next one.