In this chapter, we're going to go ahead and implement conversations schema, conversation functions, and the selection screen that will help us test the new functions which we created. Let's start by adding the conversation schema inside of packages backend convex schema dot ts and in here let's go ahead and let's add conversations and let's go ahead and add a simple define table inside the first property that will be required is the thread id Now this thread ID will be used to track the convex agent generated chat that will be happening. You won't be able to fully understand what this is used for now but later when we actually add the AI chatbot which will be very soon you will see why this thread id is required. It will basically be a reference to a conversation that the AI is having So this way we are able to have both human chats and AI chats in one and so that we are able to fetch them as needed. Now let's go ahead and let's add organization ID which will be a type of string and then let's go ahead and let's add contact session id which will be a type of the id contact sessions basically a relation to our contact sessions table and then let's go ahead and create a status field which is a union which can be unresolved, escalated, or resolved.
And now let's go ahead and let's add the indexes which we are going to need. The first index is by organization id using the organization ID field. The second one is by contact session ID using the contact session ID field. The third one is by thread ID using the thread ID field and the last one is a composite index of status and organization ID using the status and organization ID. After you have added this schema just make sure you haven't accidentally misspelled organizations or contact sessions here and then let's go ahead and run turbo dev in the root of our app and focus on the back-end task to ensure that the schema is validated, indexes are added and convex functions are ready.
After you've done that go inside of convex public and go ahead and create conversations.ts. Now inside of here let's go ahead and let's import mutation from generated server and let's go ahead and let's import v from convex values. Now let's go ahead and let's create a mutation here and let's prepare our handler. And now inside of the arguments we're going to accept two things. The first one is going to be the organization id in which we want to create this conversation and the second one will be our contact session id which we store in our local storage.
Now in here let's get the context and the arguments for the handler function and the first thing we're going to do is we're going to fetch the session using context.database.get and simply pass in arguments contact session id. So thanks to this unique way of ids per table, convex immediately knows that this will be a type of session. So no, you don't even have to define the table. And now we have to verify if this session is correct. So if there is no session available, that's the first red flag.
But even if the session exists, we still need to make sure it didn't expire. So let's make sure that it didn't surpass today's date. And now let's throw a new convex error here which I'm going to give a code of unauthorized and a message of invalid session. And we can import convex error from convex values. If you're interested to learn more about the convex error, you can go inside of their documentation, functions, error handling, application errors.
And in here, you can see how convex error can be used. So you can just pass a normal string if you want to, but you can also pass in the payload data like this with message code or any other properties you need. And then you can read that on the front end and then you can display some different UI states. So now that we have this validated, let's go ahead and let's actually create the conversation ID. So const conversation ID will be await context database insert into conversations Contact Session ID which will be our Fetched Session underscore ID so we use the document we just successfully fetched and Status will be unresolved by default.
Organization ID will be the one from the organization here and let's go ahead and do const thread ID 1 2 3 and I will add a little to do replace once functionality for thread creation is present and let me just fix this there we go And then pass in the thread id here and that should all be good and return the conversation id here. Just like that, perfect. Now just make sure that your convex functions are ready, so everything is working. And let's go ahead and set up widget dev. Let's head to localhost 3001.
By default, you're going to get organization ID is required so simply head to your clerk dashboard going set of organizations and find a working organization ID so you can go ahead and add question mark organization ID and that working organization id and after that if you have a correct session you will be redirected to selection screen. If you don't have correct session you are going to have to enter your email and username. Now let's go ahead and let's develop the selection screen. So inside of packages here, my apologies, inside of apps, widget, modules, widget, UI, screens, I'm going to copy the error screen and I'm going to rename this widget selection screen. I'm going to rename it here widget selection screen and I'm going to remove the error message and I'm just going to say selectionScreen and we can remove the alert triangle icon.
Now let's go ahead inside of our widget view component which is located inside of views widget view find the selection factory key and add widget selection screen. Let's go ahead and just make sure to import this. You don't have to do this but I'm just going to go ahead and quickly replace this with modules. I like my imports that way. And now you will see selection screen after you successfully validate and verify your organization.
Now let's go ahead and make our selection screen look like this. So we are primarily going to develop the start chat one because That is the only one which will actually have the functionality. So let's go inside of widget selection screen and let's focus on the body right here. So I'm going to go ahead remove the inside of this div And I'm also going to remove the text muted foreground. So the only class name that we need are Flex, Flex1, FlexCall.
Items center and justify center are actually not needed. So we need GapY4, Padding4 and let's add OverflowYAuto. And inside of here let's add a button from our workspace UI components button and let's go ahead and add a div here class name flex item center gap x dude render the message square icon, give it a class name size4 and a span start chat. Outside of that div, render a chevron right icon. So both of these are imported from Lucid React.
Now let's go ahead and modify this button by giving it a class name, height 16, full width, and justify between. Let's go ahead and give this a variant of outline and on click for now just an empty arrow function and just like that you have the start chat button and change this to message square text icon from Lucid React so you can remove the square icon. I think it looks just a little bit better. There we go. That is our button.
Now let's go ahead and let's implement the handle new conversation click here. So what I'm going to do first is I'm going to go ahead and do const setScreen and I'm going to do use atom from Yotai and use the screen atom Then I'm gonna go ahead and grab my organization ID from use set atom organization ID atom And then I'm going to get my contact session ID from use Adam. Oops. This is use Adam value to to use Adam value from your day. This below is use Adam value as well.
And this one will use contact session, ID Adam family and pass in the organization ID or an empty string. Like that. And now let's go ahead and let's create our mutation so create conversation will be use mutation from convex react and passing the API from workspace back-end generated API API public conversations create just like that. Then let's do const handleNewChat or newConversation. Let's make this an asynchronous method.
First things first, let's check. If we don't have contactSessionId, we're going to set the screen back to out. If we don't have organization ID, We're going to set the screen to error. And we're going to set error message, which we also have to modify here. Set error, set error message.
Use set atom, error message atom. Missing organization ID. So the user knows what happened here. And let's do early returns for both of those cases, even though at this point, you know, definitely we should have both of them and in fact this should be the order right but since we have that loading screen this technically should never happen but it will help with our type safety And now let's go ahead and let's open try and catch here. In the try let's go ahead and do const conversation id and let's make it await create conversation and passing the contact session ID and the organization ID.
As simple as that. And then let's do set screen and let's change it to chat. And now inside of Catch here, what we're going to do, well, I think we can just confidently do set screen and just navigate to out. I mean, you could look at the error. Let me just see the type of error will be unknown.
So yes, the first thing you're going to have to do here, if you want to check it on the client, is you should check if it's instance of convex error, like that. But I think we can just confidently return to out because if anything goes wrong here, it's most likely out related, right? Great. So let's go ahead and check it out. So now I'm going to go ahead and add the handle new conversation to be the on click here just like that.
And let's also just make a simple is creating conversation here. So const isPendingConversation. Let's just do isPending. And set isPending useState false. Make sure you have imported useState from react.
So when we click here, set is bending will be true. Actually let's move it to this part like that. And in the finally set is bending will be false. And then disable this button if it's bending. Just like that.
So now when you click on this, there we go. I was immediately redirected to chat. And if we go inside of our dashboard in convex, instead of echo tutorial here, instead of our data conversations, we should see one conversation with thread 123 status unresolved, and organization ID, contact session ID, which should be a direct reference to our contact session that I'm logged in with and all of my metadata. Amazing, That is exactly what we wanted to do. So now let's go ahead and use this time to also implement one more conversation function.
And so we also add the conversation ID atom into our page. So what I want to do now is I want to go inside of my widget Adams right here. And now let's go ahead and let's add the conversation ID Adam. So I'm going to go ahead. Yeah I kind of weirdly structured this because none of these are really different from one another.
We can just keep them all together and just Adam as we go along. So const conversation ID Adam will be Adam, which is a type of string or null and null by default. And actually it can be a direct type of conversations, right? Because that's what it's actually going to be. So now add the conversation ID Adam here.
Go back inside of the widget selection screen and let's just separate the setters here. Set conversation ID, use set Adam, conversation ID Adam and then we have the set conversation id here before we send the user to the chat because if we send the user to the chat it will not be able to load any conversation id perfect so now that we have both in store let's go ahead inside of our packages backend convex and public conversations here and let's go ahead and export const get1 to be a query and the arguments are actually going to be exactly the same so let's go ahead and just add them here let's add the handler and let's destructure, I mean just obtain the context and the arguments in here. And just as in the first example, the first thing we're going to do is validate the session. So let's just go ahead and make sure the session is valid or throw an error if it isn't valid. And you know the more we add this the more are we going to have the need to kind of you know make this abstracted so that We can just call some function to check this for us.
But for now I'm just going to repeat myself. But yeah there is a way to make this obviously better instead of just typing all over again. So now what we have to do is very Simply check if the conversation exists not by using. Yes we don't need organization ID for this. We just need conversation ID which is a type of ID conversations just like that.
So my apologies. It's not the same argument. The contact session ID is the same argument but we are actually looking for the conversation ID. There we go. So if there is no conversation ID, my apologies, no conversation, Just return null.
We were not able to find anything. And now here's the thing. You might think, okay, and then just return conversation, right? Or just, why didn't that, why did I even specify this? Right?
It's gonna be null. I can just return it like that. Well, keep in mind, this is public API. We need to be careful about what we are returning here. So the conversation, as you can see, has information about the contact session ID.
So it wouldn't be the smartest idea to send that back to practically whoever figures out they can call the get one endpoint. So instead of that, we're just going to return the minimal useful information here, conversation underscore ID, status, conversation dot status and thread ID, conversation dot thread ID, which we are still not using for anything but this will basically be the key to load messages right perfect so we now have the get one method and now let's go ahead and let's develop the chat screen so I'm again gonna go inside of the widget instead of my modules UI screens and I'm gonna copy the error screen and I'm going to rename this to widget chat screen. Widget chat screen, you can call it conversation screen, conversation ID screen, whatever you prefer really. Let's just remove this, let's remove item center and justify center we are definitely not going to need that and text muted foreground as well and let's just say chat screen And we can remove all of these here. Now let's go and set up the widget view and let's find chat, widget, chat screen, Like this.
Perfect. Now that we have this, let's go ahead and let's try our selection again. So just refresh your widget and you will go through the same flow. So verifying organization, verifying session and click start chat. And there we go, you're now redirected to the chat screen which now looks identical, right?
So let's go ahead and slightly modify the chat screen now. So the first thing the chat screen is going to have differently is the widget header here. So let's go ahead and give it a class name, flex-items-center and justify-between. And then in here let's go ahead and do flex-items-center and gap-x of 2. And then inside of that div let's add a button which we can import from workspace UI components button.
Give it a size of icon. And let's go ahead and render arrow left icon from Lucid React. And let's give it hover. Actually, no, we don't have to do anything. I think it's okay just like this.
So now you have this back button. But one thing that I don't like is how it looks and there actually isn't any variant that we can use because all variants look kind of bad here even when you use ghost and looks kind of weird. So let's go inside of button. You can go by command click or inside of packages, ui, source components and find button here and let's go ahead inside of the variants here and let's add transparent so let me show you where this is inside of button variants cva find the variants variant and just below the link and now we're gonna create our own variant. Bg transparent text primary foreground hover bg transparent hover text primary foreground with 80 opacity.
Just make sure you didn't misspell any of these and then give this a variant of transparent. There we go. That's subtle and I like it, Perfect. And then outside of this div, we're just going to have a paragraph. My apologies, inside of this div we will have a paragraph which just says chat.
Like that. And if you're wondering, okay, what will be here? Well, for now, what we can do is just a button and we're just going to add a menu icon from Lucid React and give this a size of icon and a variant of transparent. So Just this for now, nothing more. And now let's go ahead and let's find a way to actually query the chat, right?
So what we can do is we can get our conversation ID by using useAtomValue and conversationIdAtom. And then we can go ahead and actually fetch the conversation. So const conversation is going to be use query from convex react API from workspace back and generated API dot public dot conversations and get one. And now inside of here. This is how we have to do this.
OK. So let's just open use query. Now here's a little bit of a problem. Even though we know that we're going to have conversation ID at this point, we have to check if we have it. So if we have conversation ID, in that case, let's go ahead and let's pass in the conversation ID.
And let's do the contact session ID, which we also need. But using let's just contact session ID, use atom value contact session ID atom value, and we need the organization ID atom. So let's just make sure we have those three. And then let's grab the organization ID or empty string like this. There we go.
Now we have everything we need. And the alternative will be skip like this. And let's just ensure that all of these are correct. So I'm just going to go ahead and check if we have conversation ID and if we have contact session ID. And let me just add this.
There we go. Like that. That is how you create an optional conditional query, right? If basically, if it's skipped, it's not going to run. So only if we have conversation ID and contact session ID are we able going to be able to even pass these arguments.
So that's why we are doing this. All right, great. And now Let's simply go ahead and let's just JSON stringify the conversation. And there we go. You now have the fetched conversation.
So if you go back, oh yeah, I didn't implement the back function. Let me just implement that. So for that, we're going to need a setter. So const setScreen, use setValue. How do you...
I keep forgetting how to use. Just a second. So it is use set Adam screen Adam. And let's also do set conversation ID. Use set Adam conversation ID, Adam.
SetConversationID, use setAtom, conversationID, atom. So now that we have these two setters, Let's go ahead and let's implement on back functions. So const on back, set conversation ID will be null and set screen will be selection. So kind of like a reset. And then just go ahead and give this on click on back.
And when you click this, there we go. Perfect. Obviously, if you refresh from here, you will be redirected back to this page. But that is basically what I want us to achieve. I want us to load our newly created conversation on its individual page, and I want the option to create it from here.
Perfect. So now the more you click, the more data you're going to have here. But in the next chapter, we're going to finally make use of that thread ID so that it starts to make sense, right? Because this will be used to load our messages. But you don't even know how do we get the message schema now?
Well, that's the cool thing about convex and their agent module or agent component is that they will handle all of that for us. So it's truly a magical component and it will both allow human conversation and AI conversation, perfect. So I think that marks everything we wanted to do here. But we are not entirely done, you know, with our API, of course. We have probably noticed that in our conversations.
So for example, every time we fetch get one, what we could do is we could refresh the contact session because it's active, right? Every time we do create, we should also refetch our session because it's active. That way, if the user is active, they don't have to worry about suddenly expiring. Those are the things we have to watch out of and that's where we're going to explore our internal functions and internal queries and how we can do that. Great.
So let's go ahead and merge all of these. So this is 13 conversations. I'm going to stage all changes, 13 conversations, commit. I'm gonna go ahead and open a new branch, 13 conversations, and I'm going to publish the branch. And now let's go ahead and let's open a pull request and let's see if we have any big issues with our code.
And to end the chapter here we have the summary by CodeRabbit. We introduced chat functionality within the widget including a chat screen and selection screen for starting new conversations. We added back-end support for creating and retrieving conversations with session validation and error handling. We implemented a new database table for storing conversation details including status and indexing for efficient queries. So in here we have the sequence diagram which briefly explains what we are doing.
The user clicks start a chat on the widget selection screen we call the create mutation with organization and contact session id and the back end convex returns the conversation id. After that we are using our state management to set the conversation ID atom and we switch to the chat screen. The widget chat screen then uses the get one query passing the conversation ID and the contact session and returns the conversation summary to the UI. In here as in the previous chapter we are still getting this recommendation to find a better way to use this organization ID and I do agree but I will just continue like this for now because we are handling the lack of contact session ID and organization ID accordingly. So it is OK.
It's just not very convenient. The loading state will be handled, but not like this. The loading set will be handled using a special use messages hook that we're gonna add later on. In here, it recommends, yes, in here, it found a very good issue. So the contact session ID actually makes no sense here.
Right? The only thing we're doing is, did it expire or not? But we didn't even check if that conversation belongs to that contact session ID. So yeah CodeRabbit just saved us from a very serious security issue here. So that's something that we are definitely going to implement.
I'm going to do that the first point in the next chapter for us to do. Great and in here it also recommends verifying the organization so the only thing I'm not doing that here is because remember we need to fetch a third party API to do that clerk's third party API. So just because of that I'm not doing it. And in here it recommends a better placeholder more random one but it's OK the way it is. So one very serious security issue right here.
And we're going to do that in the next chapter to make sure our app has no such security issues. So let's merge this for now and then let's go ahead and change to our main branch. Let's go ahead and let's synchronize the changes to make sure everything is up to date here. So you can use that button, you can use this button right here as many times as you want and just confirm that your graph shows 13 and then that we merged it. That marks the end of this chapter and see you in the next one.