In this chapter, we're going to learn how to load our widget customization settings from the previous chapter into our separate widget app. We're going to start by doing a very simple default greeting message change. And then we're going to create public widget settings functions so that we can call them and query them within our separate widget app. And then using those widget settings, we will be able to load suggestions into the widget chat box. Let's quickly recap exactly what we did in previous chapter so it's easier to understand.
So I have TurboDev running and I'm going to go and visit localhost 3000. Inside of here this is the last thing we developed. This widget customization screen allows me to change the greeting message, allows me to add up to three suggestions and if I connect my voice assistant it will also show me those settings. So just to remind you, you have to connect your WAPI account using your public and private API keys which you can find on the WAPI dashboard or you can visit your AWS secrets manager and in here you can find the private API key and just add it here and your public API key and just add it here and once you've successfully connected you will see your phone numbers and your AI assistants. This is our white labeling feature.
And once you do that, you can see inside of here, you can modify your voice assistant settings. Now let's go ahead and let's actually display this inside of our widget chat box. So if you go to localhost 3001, which is where we are developing our widget application, you will see this error organization ID is required. So just a quick reminder, you can find a valid organization ID by going into clerk organizations, selecting one organization that you have and copy the organization ID. And after that, let me go ahead and just go inside of some random source code here so I can show you how you're supposed to do this.
So Localhost 3001 and then you will add question mark organization ID and you would paste this here. So that's exactly what I'm going to do here. Localhost 3001, organization ID and there we go. After that I can verify the organization and I can create my account. So Antonio and AntonioExample.com.
This creates a complete new session for me. And this is what we're going to change now. So when I click start chat, you can see that this is the default greeting message even though I clearly modified the greeting message right here for this exact organization. So just be careful that you're using the organization ID that you have modified the general chat settings for. So be careful when you copy your ID here from clerk, make sure that you didn't copy the old one because then you're not going to have these settings.
So what we have to do now is we have to go inside of our conversations.create method and we have to modify this message. So that's an easy thing for us to do. Let me close this. Before you start any code, make sure that you are on your main branch and make sure that the last thing you did was widget customization right here. Perfect.
Now let's go ahead and go inside of our packages, backend, convex. Let's go inside of public, conversations, and let's find create mutation. The create mutation is the one which writes the default message. Hello, how can I help you today? And as you can see, we have added a to do here.
Later, modify to widget settings initial message. So that's exactly what we have to do. So just before we load the thread ID and before we throw this error, actually after we throw the error, let's go ahead and let's load the widget settings. So const widget settings is going to be await context.database query widget settings with index by organization ID, get the query and inside of here query equals organization ID arguments dot organization ID and let me just fix the capitalization here and there we go. And make sure you look for the unique one.
And that's how you get the widget settings for the organization where you are trying to create a new conversation for. And once we obtain those widget settings, what we can do here is widget settings question mark dot greet message or the default message and you can now remove this to do. So if the widget settings exist and If the greet message has been set, we're going to use that greet message. Otherwise, we're going to use the default one. So now let's go ahead and try and start a new chat and here you see now it's Hulo123 because that's exactly what I changed here.
So if I just add, hi there, how can I help you? And maybe add a couple of these question marks and click save settings and then refresh here again and click start chat. That's gonna be the newest greet message. So this was a very simple thing for us to do, that's why I wanted to do it first. So we get a quick win early in this chapter.
Now what we have to do is we have to load the suggestions. This is a tiny bit more complicated. Let's start by creating the public widget settings functions. So inside of our convex here we are already in the public folder. Let's create a new file widgetSettings.ts.
Inside of this widget settings let's go ahead and let's import values from convex and let's import query from generated server. Let's export const get by organization id query arguments are going to be organization ID, which are a type of string. After that, let's add a handler, which is going to be an asynchronous method. The handler will have the context and the arguments. Inside of here, we are simply going to fetch the organization settings, the widget settings exactly the way we did here.
So you can copy this and just paste it here. And it will work perfectly because we have the same organization ID arguments. And after that just return widget settings. Simple as that. Now that we have created this we can now use it instead of our loading state because remember every time you load a widget we have a sequence of things that we are loading.
We are first loading the organization, we are then loading the session and then we are validating those things. So what we have to do is we have to add one more step here which is after we have verified our organization, let's also attempt to load this organization's settings which we can now do thanks to this public function which we have just developed. Make sure you save this file and quickly head inside of your backend here just to confirm that all functions are ready and that you have no errors. Now that we have this, let's go ahead and let's add the widget atom for the widget settings. So I'm going to close everything here and let's go inside of packages, my apologies, inside of apps, widget.
Let me see, modules, widget, atoms, widget atoms.ds. And inside of here, let's go ahead and let's add export const widget settings atom it's going to be an atom and by default it's going to be null And now let's go ahead and give it the type that it can be. So what I want to do here to make this type safe is attempt to use the document from workspace backend generated data model. So the same place we import the ID from. And let's try and make this widget settings like this or null.
So now this widget settings atom should be organization ID, greet message, default suggestion. Yes, but we could omit a few things here because the only thing we actually need is greet message, default suggestions and WAPI settings. So let me try and use the TypeScript omit function If I remember how to use it correctly and let me try and let me try to admit the organization ID and when I hover now, okay, organization ID is still present. So I don't think I'm using this correctly. Okay, let's leave it like this for now.
And I'm going to come back to this in a moment. But I think this should be okay. In the worst case scenario, we can type out the exact fields we need ourselves. Now that we have the widget settings, Adam, Let's go inside of the modules, widget, UI, screens, and let's go inside of widget loading screen because this is the place where we actually load our settings. In here we already have a couple of steps.
So I think the last one is step two actually. Yes, the last one is step two. So what we're going to do now is we're going to add one more query here. So far we have only had actions, I believe. We didn't have any queries.
Yes, actions and mutations. So let's go ahead and let's see what is the best place to add this I think the best place would be to develop step three right so this is step two let's do step three here. Step three load widget settings and in here what I want to do is I want to get the widget settings by adding use query. I'm going to add API dot public dot widget settings get by organization ID. And in here I have to use the organization ID.
Let me just understand that do we have an organization ID at this point. I think we do. Yes we do. And I have to import use query from convex react. So organization ID.
Let me just check where am I getting organization ID from. From here. All right. So what I'm going to do then. Is I'm going to do the usual if we have it then use it otherwise skip it.
Let's do it like this. If we have organization ID we are going to use it otherwise I'm going to skip. I think this will work. And now we have the ability to load our widget settings. Once we have the ability to load our widget settings, let's go ahead and actually load them.
So I'm opening a new use effect here. And what I'm going to do is I'm going to first confirm if the step is not settings, return. Let me just confirm if we have the ability for a step to even be settings. It's been a while since we developed this so I'm not quite sure but inside of your init step you should have settings. As you can see that is step 3 here which also means that instead of your step 2, validate contact session, once we validate here the next step should be settings.
It shouldn't be done. So make sure you fix that. Go inside of step two, validate contact session. If there is no contact session ID, change the set step to settings. And inside of here, validate contact session change the set step to settings and instead of catch settings as well.
So step 2 can only go to step 3 it cannot go anywhere else And this is step 3 that we are developing right now. So instead of step 3 let's go ahead and first set the loading message here to be loading widget settings. And now let's check if widget settings is not undefined, which means it's no longer loading. Let's go ahead and do set widget settings. And we don't have widgets, we don't have this set widget settings because we have to add that atom here.
So let's quickly go up here with all of our other atoms. Let's just do const setWidgetSettings use setAtom widgetSettingsAtom, the one we just developed. So make sure to import it from modules, widgetAtoms, widgetAtoms. And now we have setWidgetSettings here and we can pass in the widget settings inside of here. And it looks like the TypeScript is completely okay with this.
So I don't think we have to modify anything here. And let's just add set step. And for now it will be done. And now let's add the dependency array here. So we need the step, we need the widget settings, we need set widget settings, and we need set loading message.
Usually you don't add setters to dependency arrays, but this isn't useState. This is your type, so I'm not exactly sure what the rules are here. But I think that the only thing that's left is setStep. And I think that is it. All right, So now what we should have is after we validate this session, we should have this widget settings atom populated with the settings that we are customizing.
So let's just see if I do a refresh here. I couldn't even see it but I think for a second there. There probably is for a second that this message loading widget settings. I'm not sure what's the best way for us to test if that is true exactly. Let's just try it out.
So what I want to do now is I want to go inside of start chat and if we have those widget settings we should display the suggestions here. So the easiest way to do this is by going inside of our widget chat screen component right here inside of apps, widget, modules, widget, UI, screens, widget, chat screen. And in here what I'm going to do is I'm going to get my atom value for the widget settings. So const widget settings will be use atom value, widget settings atom, the new one which we just created, make sure to import it here. And once you have the widget settings, let's go ahead and let's create a memo variable, so const suggestions here, use memo, which you can import from React, make sure you do that.
And in here, let's just prepare an empty use memo. And let's do, if there is no widget settings, meaning that the user didn't change any settings at all, we have nothing in the database, we just return an empty array, no suggestions to be made. Otherwise, we have to modify what is now an object into an array, because remember, instead of our schema, the default suggestions are an object, right? So we have to transform this into an array. So let's go ahead and do the following.
Return object.keys, widget settings.defaultsuggestions.map, get the individual key here. And then inside of here, let's return widget settings.defaultsuggestions, open square brackets, as in we are trying to access this specific index inside of this array key as key of type of widget settings dot default suggestions. Just like that. And inside of the dependency array, add the widget settings. Now that you have these suggestions, you should be able to render them.
So let's copy the suggestions here and let's find a place to add this to. I would do it after we close the AI conversation right here to do add suggestions. So right below this let's just do json.stringify suggestions null two and if we have them there we go suggestion one suggestion two and then this. This is exactly what I have added here. Suggestion one, suggestion two, and then blah, blah, blah.
So exactly what I added is what's written here. So now what we have to do is we have to, well, just one thing, in case this is not true for you, try just rendering the widget settings. And then in here, you will see the entire widget settings. So if this is null for you, it means something is happening here inside of your widget loading here. Make sure you didn't pass the invalid organization ID here or something like that.
Make sure that your get by organization ID query is correct. Make sure that you have .unique, that you are returning this, Those things, right? But this is fairly simple. It should work. I don't think there are any rooms for mistakes here because it's pretty straightforward.
But yes, just try to retrace your steps here in case you just can't figure it out why it's not working. And also try going inside of the convex dashboard and try finding the widget settings there. Maybe they weren't created in the first place. But if you're able to see your widget settings right here, right, if they have a certain value That means it works. You developed this in the previous chapter, right?
Another thing that could be issue is you have an invalid organization ID. So double, triple, quadruple check that if it's not working for you. And once you've done all of that, you should be seeing your widget settings here inside of your atom because we add that to the atom in the widget loading screen using the use query where we check if we have the organization ID and we do that instead of the use effect right here. Perfect. Now I'm just thinking about one thing here.
Maybe I should also check if I have the organization ID here. Even though technically I don't think this can ever go wrong because of this check right here. Just in case maybe I should also have this. Let me check and refresh if this is messing anything up. It's not.
Yes, I would recommend maybe having this. Simply because this can be... I'm not sure what's the initial state of the widget settings is it null or is it undefined perhaps undefined is only while it is loading. So I think this should all be okay. You can also remove it.
I don't know. I think it works okay right now. Now that we have these suggestions, which is the one we actually care about, let's actually render them. So the cool thing about this is you already have all the components needed for this. Instead of your packages, UI source components, you have the AI folder and we added suggestion.dsx.
We haven't used it up until this point, but this is where they will come in handy. So let's go back inside of the widget chat screen and let's go ahead and let's import our AI suggestions. So I'm going to go here to the top and I'm going to import AI suggestion and AI suggestions from workspace UI components AI suggestion. And now it's time to use them. So let's scroll back down to where we JSON stringify our suggestions.
And let's add AI suggestions here. Let's give it a class name flex-full with flex-col-items-end and padding 2. And inside of here let's do suggestions.map, get the individual suggestion. If there is no suggestion, let's go ahead and just return null. So in case it's something invalid, let's not render that.
Otherwise, let's return an AISuggestion component, which is a self-closing tag, and let's pass in the key to be suggestion. Let's pass in the onClick here for now to be an empty arrow function and suggestion will be suggestion like this. Just a single suggestion. Suggestion. There we go.
Let me just see what the error is. So duplicate identifier suggestions. Oh, it looks like I already had this at some point, but I forgot to use it. So okay, we already had it. No worries.
Now let's just go ahead and check it out and there we go. Suggestion 1, Suggestion 2 and Suggestion 3. You can see them rendered right here. The problem is when I click on them, nothing happens. Now let's develop that something to happen.
So on click form set value message suggestion should validate true should dirty true should touch true and and then form handleSubmit passing the onSubmit method and autoExecute just like that and now when you go ahead and click on any of these it will automatically submit And you can see of course this is completely unclear for the AI. It has no idea what this is. But now you have the power to suggest to your users whatever you want them to ask. What is your pricing plan? Or maybe how are you?
Or are you a real person. Whatever you want to be here, just save the settings. Let's refresh again And there we go. Now we can ask what is your pricing plan? And I already forgot if we had this in our knowledge base or not.
But basically the best thing to add here would be to add it to give it questions that you have answers for in your knowledge base. So I think that I have removed them because we tested removing embeddings, but go ahead and add any TXT file, like Frequently Asked Questions, which you can find in Echo Assets. Let me just go ahead here. So instead of Echo Assets here, Knowledge Base, I added a bunch of examples. So just choose one.
Text files are super easy to work with and you can open them yourselves and see if they are correct or not. So just add one of those and then add frequently asked questions to be one of relating to those. Now there is one problem. So these suggestions take up a lot of space at some point. You can see how this starts to become a little bit weird.
So what I would suggest is only displaying suggestions if you have a certain number of messages. So in here what I'm going to do is I'm going to open two UI messages, messages.results or an empty array, question mark.length is equal to one. So if only the greeting message has been sent, only then render the AI suggestions. You're gonna see how this looks now. So you can see now there are no more suggestions.
But if you are just at the start of the conversation here, you can ask any suggestion here. Keep in mind that this will be a much smaller screen, right? It's going to look something like this and it's going to be even shorter in height. So that's why you kind of need to limit when to show these suggestions. You can also change this to be instead of beneath one another, next to each other and then allow the user to scroll if you want to.
But I think this is a more recognizable way of suggestions. So yes, that's why I added this. When you are on the first message, you can ask for a suggestion, but after that, suggestions disappear. I think this is also kind of the usual way of how suggestions work. This is exactly what I wanted us to do in this chapter.
So we can now remove to do add suggestions from here. I think that's all we actually needed here. I am a little bit skeptical about this part here. Widget settings undefined simply because I'm not sure what's the initial state of the widget settings. I'm hoping it's null.
Yeah, I don't know. I'll have to research a little bit in my next chapter just to give you the correct information about this. What I'm worried is that this doesn't actually accidentally skip the widget settings if they haven't loaded yet, if they haven't even started loading for some reason. We'll see. But I've tried five times in a row now, it works perfectly so I'm pretty sure it's okay.
But do keep an eye on this. Now let's go ahead and see if that's all we wanted to do. We created public widget settings functions and we loaded suggestions into our widget exactly what we wanted to do. Let's open a pull request and let's merge it. So 27 widget config.
Let's go ahead here, let's stage all of these changes here. Seven widget config. Let's go ahead and click commit. I'm going to open a new branch. Twenty seven widget config.
And I'm going to publish this branch. Then I'm going to go ahead inside of the pull request here. And I'm going to open a pull request. And here we go. So 27 widget config.
Let's create a pull request and let's review the changes. And here we have the summary by CodeRabbit. The chat now shows clickable AI suggestions to quickly start a conversation, selecting one out of fills and sends the message. Assistant greeting is now personalized based on your organization's widget settings, with a sensible fallback. Widget loading flow now includes a settings step that fetches and applies your organization's configuration before completing.
Widgets supports organization scope settings including default suggestions applied automatically during initialization. So exactly what we developed. Now in here let's take a look at some of the comments. So handle potential duplicate widget settings per organization. So yes, convex secondary indexes aren't unique constraints.
So using unique here will throw an error if more than one record exists for a given organization ID. That is 100% true. Convex secondary indexes aren't unique constraints. So as said here, we have two options. We can either enforce exactly One widget settings per organization at write time.
So let's take a look at our code here. So instead of our widget settings in the private here, when we do upsert, you can see just by the name, we do upsert, right? So what we check if is there an existing widget settings. If it is, we simply patch. Otherwise, we create it for the first time.
So this is exactly what we are doing. We are enforcing exactly one widget settings per organization. So that's how we fix this issue. In here they recommend switching to a more tolerant first instead of unique but I'd rather we have an error, right? Because then we know, okay, this enforce here is obviously not working as it should, we need to fix our code.
Whereas if you use first, it's not going to throw an error, it's simply going to select the first one, but the first one should be the only one or the unique one. So that's why I don't think we have to do anything here because we're doing exactly what they recommend here using the absurd method and they have the same comment here for our new public widget settings. That means that this pull request can be merged so let's go ahead and merge this pull request and once you've merged it let's go ahead and switch to our main branch here. So clicking down here, selecting main, and then clicking right here to synchronize the changes. Once you have synchronized the changes, I always recommend going inside of your source control here, clicking on the graph and just confirming 27 is the last thing you merged.
And just one thing to ease your mind, instead of our widget loading screen, I told you I'm not too confident about this, but now I am because I went to convex documentation here and I found UI patterns. Check for undefined to determine if a query is loading. So what we do here is we go to the next step only if widget settings is not loading. Not if it doesn't exist. It's completely fine for widget settings to not exist.
But I don't want to go to the next step until I've at least tried or attempted to load the widget settings. Completely fine if we get back null. That's fine. I just wanna make sure that even users who, even organizations who don't have widget settings can still get past this step because widget settings are not required. And looks like that is exactly what's happening.
The useQuery React hook will return undefined when it is first mounted, before the query has been loaded from convex. Once a query has loaded, it will never be undefined again, even as the data reactively updates. I just clicked somewhere, but then it goes and says, undefined is not a valid return type for queries right so you can use this as a signal for when to render loading indicators and placeholder ui so that's why this is completely safe I will double check it just in case with a completely new organization which doesn't have widget settings and I recommend you do that as well but I'm 99.9% sure this works just fine. Perfect. I believe that marks the end of this chapter so let's just mark this, this, this and this as completed.
Amazing, amazing job and see you in the next chapter.