In this chapter, we're going to be building our embed script, which will allow our users to put our chatbox within any application they own. In order to do that, we're going to create a third application within our monorepo called embed. And this will not be a Next.js app instead it will be a Vite however you pronounce it app. We're then going to create the actual embed.ts script which will load our localhost 3001 widget app within an iframe and it will give it some buttons such as close button, open button and things like that as well as some auto resize options. We're then going to build the build package.json script and we're going to use that to create a minimized embed script which we can then test and see if it works in a random empty HTML page.
So let's start by creating a new embed Vite app. So I'm going to go inside of my apps here and I'm going to create a new folder called embed. Within that folder I'm going to create a package.json. Let's go ahead inside of here let's give this a name of embed like so let's go ahead and give it a version let's give it a type let's give it a private property and then let's create scripts. Now the scripts will be the following.
The most important one for now is going to be a VitBuild. This will be used to create the minimized script that will be inside of a dist folder and then we're going to copy that and we're going to store it somewhere where we can later load it in an empty HTML page. So this is the only one that we're actually going to need for now. So we can only leave that. And now let's add some dev dependencies here.
So types, node version. What I like to do is I like to search for the versions that I'm using within my app here. So it's 20. So I'm going to put 20. In fact, I'm just going to copy from here.
So I have the same version. Then I will have at workspace, S lint config. And I'm just going to use workspace and I'm going to use the little carrot here like so. Then I'm going to add TypeScript config. In here I'm just going to put an asterisk.
For my S-Lint version, Let me search again what's being used here. So 9.20.1. So I'm gonna add this. TypeScript. I think I have multiple versions of TypeScript.
Yes, somewhere I use latest and somewhere I use 5.7.3. I like being specific so I'm just going to copy this and add it here. And finally the Vite version which will be again with this little carrot here 5.0.8. Of course if you're watching this far into the future and you want to be up to date, you should probably look up the newest versions. But yes, if you're just following the tutorial, feel free to use the exact versions that I am using.
And once you have that you can go ahead and just run pnpm install in the root of your app here and I think that that should update the package log file with all these new packages. There we go. So immediately pnpm lock apps embed perfect. Now that we have that and we have the node modules in here let's go ahead and let's create the well step by step let's start with the slint config right So slint.config.js is going to import base config from at workspace slint.config forward slash base.js. And let's export default and simply spread base config here.
Just like that. Then what I want to do next is create the TS config so we have the TypeScript rule TSConfig.json. TSConfig.json will be an object which is going to extend our workspaces typescript config base.json. Then let's add the compiler options and the compiler options will have the following lib. We're then going to have module and the rest of the properties.
So target, module resolution, no emit, skip lib check, true. Then let's add include. So what we're going to include for now will be embed.ts which is the script we will create, white.config.ts or vt. Then we're going to have vt-environment.d.ts. And let's actually target every ts file inside of here and then every folder which has a TS file inside.
Like this. And I think, let me just restart TypeScript server. So this is still happening here. I'm not too sure why but I'm just going to leave it like this for now. Now let's go ahead and let's create the Vite config.
So instead of embed let's create Vite.config.ts. Let's import defineConfig from vit. Let's import resolve from path. And let's export default defineConfig. Inside of here let's add build.
Why build? Because in package.json that's the only script we've added so we have to define it in the vid.config. Let's open lib object, entry, resolve, dir name, embed.ts. So this will be our entry. This is the file which we are going to write and the output will be something else.
So name of this Script will be echo widget. The file name can just be widget. Formats. Now, in here I don't know too much about this. This is one I found that works.
If you know more about these formats, feel free to use the more optimized one. For the rollup options, let's set output, extend, true. And for the server, well, we don't need anything more actually at this point. So this is enough. Now let's go ahead and let's create the Vite environment file.
So vite-environment.d.ts. Let's go ahead and add this at the top. I think this is required if you want Vite environment files to work. So this is three forward slashes here, so be mindful. And then Let's go ahead and create the interface import meta env read-only vt.VidgetUrl.
It's a type of string. And let's add interface import meta, import meta environment type. So that's our vt.env.d.ts. Now that we have this, let's go ahead and let's start creating the embed script. I think we can already kind of try this.
If I go instead of apps embed and if I create embed.ts here, and if I just do console log, hello world, like that, or maybe if I do const Antonio, hello, const something world. And if I then attempt to join the two, so console.log Antonio plus something. I'm trying to purposely make this script a little bit longer because the build script is supposed to minimize it. I'm not sure if this is enough to trigger that minimization. So the way we build is this.
Maybe this format or maybe this roll up options is what is deciding how minified this will be. Since we have package.json.build, we can build it in two ways. We can just run TurboBuild in the root of our app, but since we don't want to wait For all other apps, we can just go ahead inside of our apps embed and let's do Turbo Build here. There we go, embed build and now inside of our dist folder you should have a minified version. So you should also now have this new.turbo here which is cache and inside of here, there we go.
You can see a minified version of our script here. So inside of embed.ds, we will be able to develop a readable script, right? And inside of here, we will have a minified script, which is, you know, shorter. It is not readable, but it is something you will host inside of some CDN or somewhere or maybe just in your public folder which is what we are going to do. And then you're going to reference this file so you don't have to reference this not obfuscated longer file, right?
This one needs to be longer because it needs to be readable so you understand what you're developing, right? Perfect! So that's what we just wanted to do right now. That's an amazing first step so our Vite build is officially working. Now it's time to actually develop this script.
What I want to start with is with the config file. So config.ts. Let's export const embed config and now inside of here let's create the widget URL which will be import.meta.environment.vidwidgetURL and I'm pretty sure that This type safety comes from this very cool vitenvironment.d.ts. That's how it recognized it. And let's also give it a fallback.
So if it cannot find this, let's go ahead and simply use 3001. Why 3001? Well because if you go ahead in the root of your application here and if you do turbo dev, let's go ahead and focus on the widget. Dev, you will see that the widget is running on 3001. So make sure that you use whatever your widget is running on because that's what we are building the script for so that is the page that will load within an iframe.
And if you're wondering why don't we have embed here? Very simply because we never added the development script. So instead of the apps-embed-package.json, we only have build script. So Turbo Repo knows that it cannot run the development script here. That's why it's not being run here.
So great, now that we have this result, let's go ahead and let's add default position, which I'm going to add bottom-right as constant. So that's going to be the default position. Now that we have this, let's go ahead and let's add the icons. So instead of embed, let's add icons.ts. Since both of these icons which we are going to use, two of them, the chat bubble icon and the close icon are going to be SVG files, the best way to do this is to simply copy it from my assets folder.
You can see the link on the screen. So head inside of the embed folder here and head inside of the icons and in here simply copy that file and paste it here and you should have the chat bubble icon and you should have the close icon. Now it's time to build the actual embed script. So let's prepare that. Instead of apps, embed, let's go ahead and go instead of embed.ts.
So we've already started doing something here. Now here's the thing. I know that some of you, I think most of you, love to see me write everything from scratch. And I love doing that too. In fact, we've been doing that together for the past 20 hours, I think.
But in this particular script, you're going to see it's not really worth it to write it from scratch. I built this entire script either iteratively, I'm sorry I just butchered the word, basically through a lot of trial and error, through a lot of refactoring and fine-tuning the implementation details. And if I walk you through each line, it would take a significant chunk of time, which we could better spend on actually developing the embed app. So let's be efficient with our time here. I have added the entire embed.ts script inside of the public GitHub repo.
And I think that the moment you open it, you're going to understand what I'm talking about here. You can see there isn't much of a learning value here, right? But I will explain in chunks exactly what's happening here. So let's go ahead and just copy this script here, embed.ts and let's paste it inside. And now I'm going to go ahead and explain exactly what's going on here.
So first things first, we are importing the config from our .config and we are importing the icons from our .icons. So in the first chunk here, what is this? Why are we defining the function like that? You can see that in here we define the function within its own parenthesis and then down here we execute that function. Why are we doing that?
Well, the answer lies inside of our vt.config.ts in our format right here. So this is IIFE or immediately invoked function expression. So that's what this is. This is an immediately invoked function expression right here. And what that means is that it creates a private scope so we don't pollute the global namespace.
Which means we can initialize our main variables the iframe, the container, the button, and the state tracking without overriding any global variables. So yes that's what we do here as you can see we set the variable for iframe for container for the button and some is open state tracking the clever part here is how we grab the configuration so in here we have to obtain the data organization ID But how do we know for which organization is this script going to be used for? Well, remember inside of our web, I think that's the name of our app, it is, we have a module called integrations and in here we have constants and in here we have already talked about the way that we are going to pass this organization ID to our scripts so this is how we're going to do that we're going to load this embed.ts within a file like this and we're going to pass the data organization id with 1, 2, 3, 4 inside right and then in here once we obtain the current script we're going to get that attribute from the script from this element right here. So we are obtaining ourselves and we are getting the date organization ID.
One thing that we are also obtaining is the data position. That is if the user for whatever reason wants to move from bottom right to maybe bottom left. But if we don't have this, we simply fall back to the default position, which is inside of this config bottom right, which is the most popular one, right? In case this fails, we also attempt to find this using a different selector. So just some additional logic here.
Great. So what do we do next? What is this init? What is this render? So that is DOM initialization and rendering.
Basically, we have to check if the DOM, the document, is ready before rendering. This is crucial for avoiding any errors. And then the render function itself creates three main elements. A floating action button with smooth hover effects. So that's what's happening right here.
Echo widget button. We create the button. We give it an inner HTML of this chat bubble icon, which is just an SVG of a small bubble icon. And then we give it some positioning in the bottom right corner or the bottom left corner, right? And we give it some colors which match our app as well as some other styling.
We then give it some listeners here, which will then transform or it will trigger some functions such as toggle widget. And one thing that we're doing here, as you can see, is writing CSS in line so that no external CSS is needed here. And we also need the container built the same way that the button is built. So what does the container do? The container is what's holding our iframe and within our iframe is where we are going to load our widget.
What's important for the iframe here is that we allow the microphone, clipboard read and clipboard write. Because we have VAPI so we can listen to a microphone and clipboard read and clipboard write so we can copy the phone number if you remember because we have that function too. Great. So in here we do have some additional scripts such as build a widget URL and things like that. So let's go ahead down here to that script actually and let's see what's going on here.
So in here we are finally using that organization ID that we extracted from above. We extracted it from this right from data organization ID And now that we have that organization ID, we build the iframe URL. And the iframe URL is localhost 3001 and then a param organization ID. So the config here is set to localhost 3001. So in here, what we are doing basically is we are returning localhost 3001 and we append a prop called organization ID and we set it to a variable, one, two, three.
And that is exactly what we've been doing this entire tutorial whenever we want to test the widget. So we are now just doing that via a script. And then we have some hide and show animations here as you can see some additional functions to toggle the widget, to hide the widget, maybe some additional animations. And one thing at the bottom that we do here is a public API so that you can destroy or manipulate or reinitialize this entire widget from the inspect element or from if any developer wants to access it in their own little way. So we are kind of making this an even better script than it is.
Great. So again, this entire script is available here. I hope I explained it in a nice way. As you can see, it's quite a lengthy script and I don't think we would learn much if I went line by line here because this isn't our usual code which we do, which is JSX, right? This is just pure JavaScript.
It's not particularly pretty code, I know, and I felt it's better explained like this in chunks rather than you watching me do it every single line here. Great! Let's go ahead and try and build it now. So I'm gonna go ahead back inside of my apps embed and let's do turbo build. And let's see, there we go.
Now, inside of your dist, you will see a much longer version of this. There we go. But still a minified version which you can now use and test out in some other app. But here's one thing I'm sure you don't actually know. So right now my dist here as you can see uses the widget URL to localhost 3001.
Sure, because that's currently where our widget app is running. But what if you deploy the widget app and it has its own domain, right? Antonio-widget.com. Well, we actually have a solution for that as well. I just completely forgot to tell you about it.
So the way you can do that is by running vitwidget url https antonio-widget or maybe for example running with widget URL HTTPS Antonio dash widget or maybe for example code with Antonio dot com and PNPM build. And now when you build it you can see that the widget URL so again go inside of this widget dot JS you can see that the widget URL, so again, go inside of this widget.js, you can see that the widget URL is codewithantonio.com. So right in here is where organization ID one through three will be appended. So that's how you can change the URL of where your widget application is hosted. So for us, PNPM build is enough because I have hard-coded it to 3001 because I know that in the root of our app, when I do TurboDev, the widget app is running on 3001.
So I know that that's what I have to load within my iframe. Now In order to test this, let's go ahead and let's add one more HTML file here. So again, it makes no sense to build this from scratch. This is just a literal testing suite of the embed.ts script. So this already works.
You can kind of already finish the tutorial here. I'm just giving you a cooler way to test this embed script. So Let's go ahead and inside of the embed, let's create a demo.html. Like so. Let's go ahead inside of our embed folder, demo.html.
So this is just a normal HTML file and it's a testing suite for the embed.ds file. Now what you have to modify here of course is the organization ID. So why am I hard coding an organization ID here? Because this is not for customers, this is for you internally. So it's easier for you to test out this embed script which we just went over.
That's why. So yes, let's go ahead and fix this now. So I'm gonna go ahead and well let's go ahead and do this first. How about this? We first go inside of package.json here in the embed and now let's add a dev script.
So dev and let's add vit port 3002. Why 3002? Well simply because in other package.json like in web, we have already reserved port 3000. In the widget app, we have reserved 3001. So the only one we have left is 3000.
I mean, we have more left, but incrementally it's 3002. But that's not enough. What we have to do now is also go inside of v.config.ts and inside of here let's go ahead and Let's go ahead and add server. Port 3002 and open forward slash demo dot html. Just like this.
And now in the root of your app, you should be able to run TurboDev and you should finally see the embed app using Vite. Now in here, as you can see, we have this button and you can see unable to verify organization. Exactly, We have a problem. That's because we are not passing the proper organization ID here. So let's go ahead and fix that.
So head inside of your one of your apps here. I mean one of your organizations. If you want to choose a premium one, choose a premium one. Maybe that will be easier for you so you can test multiple features. Let's see, is this a premium one?
It's not a premium one. Which one of these is a premium one? The free one? Yes, the free one is the premium one. Okay.
So I'm going to go inside of integrations and I will copy my organization's organization ID. We can do that from here now and I want you to go inside of your embed and go inside of the config first and let's quickly just extend the config here by also adding a default organization ID. And just add your default organization ID here. And then go inside of demo.html and change this in here too. So all instances of some hard-coded organization ID just change it, right?
Even though I think this one is just presentational, it doesn't matter. I'm fairly sure. Okay, no. Inside of here, data organization ID also change this. I think I left this because I couldn't find a nice way to modify HTML directly.
The scripts are easy to modify but for these ones you have to change them manually. So find the data organization ID, change it here and just one place above too. And then go back and save your echo widget demo. Let's refresh now. Oh, this one is much faster, okay.
And there we go. Look at this. Our app is working. I can start chat from here. Hey there, how are you?
Perfect, look at this. We have a working application on some random Vite app we've been able to integrate our chatbox. And in here, Let's go ahead and set up the conversations. There we go, we can chat with our customer in real time, all thanks to Convex. Hey there.
Look how amazing it is. I'm so impressed by this because this is like a completely different framework using Vite running our app here. And what this test suite is for is that you can kind of test this, you know, let's initialize the widget again. If you want to test this on this side, or if you want to destroy it, if you want to show it, initialize it, right? It's just for that.
It's just for testing. So you have it so it's easier for you to test the widget. But we're not exactly finished yet because there is just one more thing that we have to try to confirm that this works as intended. And that would be running Turbo, not TurboBuild, I mean, yes, but just inside of apps.embed, go ahead and run TurboBuild. So you have the latest and the newest widget.iife.js.
And then go ahead in the root of your app and run TurboDev. Okay. And now, here's what you would have to do. So, inside of your apps, embed, instead of this, you now have this minified script, which you're supposed to host somewhere. So where should you host it?
Absolutely anywhere. It doesn't matter where you host it because this script it doesn't matter if this script is private or not. You can host it on I don't know Cloudflare, you can host it on BunnyCDN, you can host it on GitHub, I don't know, wherever you want. But since we don't have that setup yet, let's just pretend that inside of widget, our public folder is the host. So let's go ahead and do that.
Let's go ahead and add widget and let's rename this to JS. You can do that. So just inside of public, widget.js. And then what I want you to do is go inside of embed and go ahead and create a landing.html as in this is some random landing page HTML. Let's go ahead and just set up this randomly.
And in here, what we would do now, oh yes, I completely forgot, we now have to kind of modify the following. So we should now go inside of apps, Web, Modules, Integrations, Constants, and we have to modify this script now, right? So this part is okay, but the actual source of the script should be different. So what source am I talking about? Well, in here, it should be source, HTTP, localhost, localhost 3001, forward slash, widget dot JS.
Right, so that's why I told you, this can be anything, right? Wherever you want to hold that minified file. If this is some cdn.cloudflare.com, sure, it will be like this. It doesn't matter. But Just for now, since this is the only place, this is the only idea I have, right?
Just putting it inside of Widgets public folder because the public folder technically serves like a CDN, I guess. I'm just using it. Imagine you got this random script from the internet. That's what I'm trying to explain to you. You can have this anywhere.
Anywhere, right? You don't have to have an XGS app running to host this app, right? But you will have to know where you have it and you will have to know where you store it like this. And then go ahead and add it to all these examples. And then you can do the full integration process for real now.
Click inside of integration here and you will see a proper script. Let me just go ahead and do that. So integrations and let's go ahead. I'm adding it to my HTML. Here it is, right?
This is the exact one I have. Let's go ahead and copy this and then I would go. Let's imagine I'm some random developer who is trying to integrate Echo instead of my project. Here I have my little landing page dot HTML and I would just go ahead and add this script here. So I'm heading to localhost 3001, the widget.js, and I already have my data organization ID prepended for me.
So now I'm just gonna go ahead and open this landing.html file. And there we go. So if you're wondering how did I open it, I literally just went in my file explorer, right click, open, and it will automatically open with Chrome or whatever browser you're using. And there we go. Test, testmail.com.
Let's go ahead and continue. Start chat. Hey there. It works. So someone just successfully added our website, our little script to their landing page.
That's what this was all about, right? That's how this is going to work. So what's important here? The most important part for you to remember will come now in the next chapter, which is deployment. And that is, you will have to know when you build this embed minified script where your widget url will be.
So what is the widget url? The widget url is not embed script url. So don't confuse just because I'm using localhost 3001 here and I'm also using it inside of my config, inside of my constants in the integrations. This is just a coincidence. This can be anything.
It doesn't matter where you host widget.js, but it does matter where the widget.js script thinks that your widget app is being hosted, right? So when we deploy to Vercel now, we will have two different apps. The web app running on one domain and the widget app running on another domain. The widget app will never be directly visited through URL. It should only be visited through an iframe.
So if you manage to get this working, amazing. The project is like 99.9% finished. All that's left is to deploy. Seriously, amazing, amazing job. Super long tutorial and I'm extremely proud of what you did.
Also if this is a problem for you, you can turn this off. This is just for development. So these are dev tools. So don't worry. Your users won't actually see this little thing here.
I think this is so cool, right? And everything just works. Amazing, amazing job. So let's go ahead and merge this and in the next chapter let's deploy it. So I'm just going to go ahead, stage all of these changes, 34 embed script, let's commit, I'm going to open a new branch, 34 embed script and I'm going to click publish branch.
Now let's review the pull request. And here we have the summary by CodeRabbit. We introduced an embeddable widget with a floating button and iframe panel. We also support configurable position, bottom right or bottom left, animated show and hide, as well as some post message driven resize and close. We auto-initialize via script tag and we expose a global API for init, show, hide, and destroy.
We added a demo page with controls and example usage. And we also added a simple landing page where we pretended to be a user embedding our chat widget. Amazing, amazing job. That is exactly what we did. Since this was a fairly simple you know pull request and we copied the snippet from GitHub.
It doesn't make sense to really do any review changes here because we went really in depth with chapter by chapter review. So I'm going to go ahead and merge this and then we can go ahead instead of our main branch and we can synchronize the changes to make sure everything is up to date. So in the next chapter what we are going to do Make sure that 34 is the last one you've merged is we're finally going to deploy this and learn how to do this once again with deployed scripts. So I believe this marks the end of this chapter. We created a VIT app, script, build script, and we tested the minimized embed script as well as pushed to GitHub.
Amazing job and see you in the next one.