In this chapter we're going to enhance our widget chat screen by adding infinite scroll functionality as well as an avatar component to render some images next to chat bubbles. So let's go ahead and start with use infinite scroll hook. Since we're going to need this hook across our web and widget applications I thought it would be a good idea to add it to the UI package because in here we actually have hooks already. So let's go ahead and add use infinite scroll.ts and then in here let's go ahead and import everything we are going to need so that's going to be useCallback, useEffect and useRef from React Now let's go ahead and let's create an interface use infinite scroll props. The first one is going to be the status.
Now the status can be the following options. Can load more, loading more, exhausted or loading first page. So if you're wondering where do I get these values from? Well, if you actually go inside of the widget chat screen and if you find use thread messages here, you can go ahead and try and do messages dot status and when you hover over it you will see loading first page can load more loading more and exhausted so those are the options and I've just transferred them here. So just double check.
I recommend that you do this and just hover and just double check that yours are the same here. You can even copy them from there and just add them here like that. Now they are in this order. It's the same thing. Let's add a function loadMore to accept a certain number of items here which is a type of number and let's go ahead and add an optional load size.
So if we want to load by 5, by 10, by 20, like that. And let's add and Boolean observer enabled, which will basically tell this hook, do we want automatic infinite scroll or only on load more button click? Now let's export const use infinite scroll hook here. And let's assign the props. And let's destructure the status load more, load size, observer enabled, set to true and load size set to 10 by default.
Now the first thing I want to do is I want to add a top element ref like this which is a use ref with a type of HTML div element and a default value of null. Now let's define the handle load more use callback method like this and inside we're going to check if the status in is can load more in that case we're going to load more how many? Well the exact load size we defined and we need to pass status load more and load size in the dependency array. Now let's add the use effect here. Instead of the use effect we're going to get the top element by referencing our top element ref which we defined seconds ago.
And since it's a ref, the way you get the value is by accessing .current. So if neither top element and observer enabled return. So if it so happens that we can't find the top element or if observer is not enabled no point in continuing forward. So just make sure you put an exclamation point in here. And then let's go ahead and let's do const observer, new intersection observer.
Go ahead and open a function like this, open an array and extract entry from here. Go ahead and open a function. If entry?isIntersecting, handleLoadMore. Like that. And then add a threshold, 0.1.
So this is basically sensitivity, right? I find it that it works pretty well with these settings and let's add observer dot observe and pass in the top element So when we reach the threshold and intersect with the top element, we're going to trigger load more, but only if top element is visible. And if, I mean, if top element was found and if the observer is enabled in the first place. And what's important is that you add a unmount function here and disconnect the observer so there aren't any leaks. And we need to add handle load more and observer enabled.
So in case you were wondering Why did load more need to use use callback? Because we're using it in use effect and that's how we memorize it and how we safely add it into the dependency array here. And now what we have to do is we have to return back the top element ref handle load more, can load more, which can simply be a status and then can load more. And then let's go ahead and just do the same for all the other statuses. Is loading more is checking loading more.
Is loading first page will check if the status is loading first page is exhausted we'll check if status is exhausted just so we don't have to do that wherever we use the hook we have this little helper here there we go all right so I just want to remove this messages.status in the widget chat screen because I see it's unsaved and it's bothering me so I'm just going to close it. Now let's go ahead and let's create the infinite scroll trigger component. So again inside of packages UI components create infinite scroll trigger dot tsx. Let's go ahead and let's add button and cn both from workspace UI. One from components and the other one from lib.
Now let's create an interface infinite, scroll, trigger, props. Can load more is going to be a boolean, is loading more is going to be a Boolean, onLoadMore will be a function, loadMoreText will be an optional string, noMoreText as well, class name, same thing, and reference an optional react reference html div element like so let's export const infinite scroll trigger and Now in here let's attach infinite scroll trigger props and inside of here we can destructure all of those props. So what I did was destructured all of them and I added default values for load more text and for no more text. So by default, the button will say load more. And if it can't find any more items, it will simply say no more items.
Now let's go ahead and let's define the text to be load more text. If is loading more, text will be loading. If you want to you can even create a prop for this but I think it's a bit of an overkill. My apologies. Else if not can load more, text will be no more text.
And let's go ahead and let's return a div. Let's give it a class name, cn flex, full width, justify, center, py of 2. And then pass class name as the optional prop and pass ref, whoops, outside, ref to be ref. Add a button, render text inside, give it a disabled prop if not can load more, so if it can't load more or if it is loading more. OnClick, it will call onLoadMore.
Size will be small and variant will be ghost. Just like that. Perfect. And now I think we're actually ready to use these two. So let's start by going back inside of the widget chat screen, which you can find inside of apps, widget modules, widget UI screens, widget chat screen.
And I just want to add my import. So let's import use infinite scroll from workspace UI hooks, use infinite scroll. And let's also add infinite scroll trigger from workspace UI components infinite scroll trigger. Now Let's go ahead and let's use the use infinite scroll hook right after our messages here. So it's all kept together.
So I'm going to go ahead and call use infinite scroll like this. And inside I'm going to pass status to be messages.status. And you can see that, well, let's first add all of them so we don't have any errors. So load more will be messages, load more. Load size will be 10, the same number that I used for the initial number of items.
If you want to, you can remove this magic number and then put it in a constant. But you can see that I have no errors here because my status matches exactly what it accepts, right? So that's why it was important for you. If you're getting errors here, simply hover over message.status and check what's happening here and then go inside of your use infinite scroll and check that you have the same here and then you will probably have to slightly modify them here if they are different but they shouldn't be. Now in here you can destructure the top element ref, handle load more, can load more and is loading more.
Whoops, is loading more. Like that, perfect. Now we have to add our infinite scroll trigger, which will also be the top element ref that will be used for the observer intersection. So usually with an infinite scroll, you would put that at the bottom of your page, but we're doing chat interface here. So it's the opposite actually.
Only when the user scrolls at the top of the chat page do we load all their messages. So we have to find the start of the AI conversation and then the start of the AI conversation content where we rendered the messages and in here add infinite scroll trigger, which is a self-closing tag Give it can load more of simple can load more Give it is loading more is loading more unload more unload more and give it the ref top element ref. And this should be handle load more, not unload more. Perfect. Let's go ahead now and let's do TurboDev here.
And let's go and load our widget component. A quick reminder, you go to dashboard, clerk, Go inside of your organizations here, find a functional organization ID and add question mark organization ID with capital I and pass the organization inside. Perfect. 24 hours has passed since the last time I had this session so I have to create a new one. So I'm going to call myself Antonio here, Antonio example.com.
Perfect. Let's go ahead and, oh, this is interesting. So after I click continue, nothing happens. Looks like we have a bug here. Interesting.
Let's see inside of my application. I let me remove my old session here and try adding new one. And then if I refresh, what happens? So it works then. Okay, but if I remove it completely and if I try Antonio example.com, one, two, three.
Whoops, I reversed the two. It adds it to the session and when I refresh it obviously works but looks like there is some issue in my out screen, inside of widget out screen. When I submit all we do is set the contact session ID. Yeah, we actually don't do anything. First of all, let's remove the test organization ID from the out screen.
Second, let's add a set screen to be set atom value, set atom, use set atom like this, screen atom. So make sure you import screen atom. And let's just call set screen after that here, do selection. That was missing. Let me go ahead and just try and see if I fix this issue.
When I refresh now, let me try again. Continue. There we go. Small bug, fixed it very easily. Let's go inside of start chat now.
And what I want to do is I just want you to type out a bunch of messages, right? So feel free to interrupt the AI. It doesn't matter, right? We're just trying to make this a bit of a longer chat so that we can actually test out the infinite load. One problem that we have is that we can't really test it.
It just didn't occur to me until now. But if you can use your chat like you used until this point, everything should be okay. One indicator that this is working correctly is the fact that it's showing no more items at the top. The reason I'm saying we can't test this is because we don't really keep the option to select individual chats. You can see I can only start a new chat.
Right, so okay, we will test this in some other chapter, but I'm pretty sure this is working perfectly fine. Nothing complicated was added here. Basically this infinite scroll trigger is the no more items text because it loads the very first message the moment you open the chat, right? We will find some other opportunity to test this, preferably in the next chapters when I implement the actual conversation list. So for now, just leave it like this.
Even if you have a bug, it will be easier for you to debug by implementing the next chapter first. All right, so one more thing I want to do I want to add the Dice Bear avatar here. In order to do that we first have to add a logo to our app. So you can find using the link on the screen my public folder and inside of here Let me just update the repository and you will see the logo.svg. All right, just update the repo.
So public folder, logo.svg. So where did I find this logo? So every single time in my projects, I use this amazing website, LogoIpsum. You can use the link on the screen to let them know you came from this video. They are like the lorem ipsum of logos.
Amazing, amazing logos you can use in your demo applications or exercises like the one we are doing right now. So basically just choose the one you like. I found the one I like and let's go ahead and save it and once you save logo.svg let's go ahead and let's add it inside of apps widget and inside of here I think you're going to have to create a public folder. So inside of widget, create public like that. And then inside of here, you're gonna have to add logo.svg, which you just downloaded.
So drag and drop it, copy it, however you want to do it. Great, once that's here, let's go ahead and let's implement the dicebear avatar component so again I'm going to do that inside of packages UI, source, components. Let's create DiceBear avatar.tsx. Now we have to go ahead and install some packages here. So let's go ahead and do turbo.
Whoops. My apologies in the root of your app, BMPMF widget. Let's add a at dice bear forward slash collection and at dice bear forward slash core. So those two packages inside of your widget application. And then you can turbo dev again.
Once you have that, let's go ahead and let's add use client in our DiceBear avatar here. Let's go ahead and let's import glass from DiceBar. Whoops, I just told you to add it to widget, didn't I? My apologies. All right, so we made a mistake.
Let's see how to fix it. We added two incorrect packages to the incorrect app. So select the package we just added and the same way we added them, we're going to remove them. Pnpmf.Vidget.RemoveDiceBareCollection. So let's do them one by one.
So first I'm going to remove Dice Bear collection here and then I'm going to remove Dice Bear core because we don't need them in the widget component. So pnpm like that. And that should reset your pnpm lock and basically everything. It's always safer to do this than to manually try and modify your locks. So now let's go ahead and do bnbmf ui and let's add Dice Bear collection and core into the ui package.
And once we do that, let's go ahead and turbo dev our app again. So turbo dev to make sure everything's running here. Now we should have packages and instead of packages UI modified. Let's go back inside of the DiceBear avatar. We should no longer have any errors here.
And now let's import createAvatar from DiceBearCore. Let's go ahead and let's import useMemo from React. Let's import the avatar component from WorkspaceUI components avatar and let's import cn from WorkspaceUI libutils. Let's create an interface dicePair avatar props. We're going to have a required seed which is a string, size which is an optional number, class name which is an optional string, badge class name which is an optional string, image URL optional string and badge image URL which is an optional string.
So this will basically allow us to have a cool little effect where we have the avatar and in the corner of that user's avatar we're going to display the flag where they're writing from so we know where they're located. That's why we're adding the badge functionality as well. Let's export const dicebear avatar. Let's assign the props here. And let's destructure all of them and let's set the size by default to be 32.
Now inside of here the first thing we're going to do is we're going to create avatar source. We're going to call use memo here to memoize it. If we have image URL passed as the prop, we're simply going to use that image URL. It's kind of like an override. Otherwise, we're going to create the avatar.
So this will be useful when we have many customers and we don't have access to their images. We could extract them from their emails but then again they never really verify that email so I'm not sure how much we should associate them with their email and giving them an option to upload an image in a customer support chat is just weird. So we're just going to use a simple avatar collection of this glass-like images which will use the seed which we are going to convert to lowercase here and then trim it to make sure it's valid with the size to create a unique avatar for each of our customer and we are not going to use any requests for that so this will create the SVG data URI so you won't need to do any network requests for this don't worry. Now let's calculate the badge size So if we are basically going to round this number, but basically we're going to take 50% of the avatar size like this. And then let's go ahead and Let's return a div.
Let's give this div class name relative inline block. Let's give it style, width, Size and height size. So they are the same because avatars are circular in our, they have the one by one aspect ratio in our case. So let's go ahead and give the avatar component itself a class name of, let's make it dynamic, CN. We're going to add border and then class name like this.
And then you can copy the style here and pass it as style. Inside let's add avatar image. Alt can be, it doesn't have to be anything. We can just say image and source will be avatar source. And then we're going to check if we have badge image URL, we're going to create a div that will render that badge image URL.
Let's add class name here, make it dynamic, so prepare CN here. Absolute, right zero, bottom zero, flex items center, justify center, overflow hidden, rounded full, border two, border background and BG background and passing the batch class name. And now let's open the style attribute here. And inside we're going to pass width and height to be the batch size which we used 50% of the avatar size from above and transform translate 15 percent 15 percent this will basically put it like in the corner of The avatar so it will look nice and then finally just a simple normal image element out can be It can be empty Class name will be height full with full object cover. Height will be badge size.
Source will be badge image. URL width will be badge size like this. Basically in the alt, you would put what this image is representing, right? So, okay, we can just say at least badge, right? So it makes sense.
This is the main image or avatar. And this is badge. So it makes sense. All right. And that's it for our Dice Bear avatar.
Now let's go back inside of our widget chat screen and let's go ahead and let's use the Dice Bear avatar. So let's scroll down here to where we iterate over messages and here we have to do add avatar component. So let's go ahead and Let's check if message.role is assistant. Let's go ahead and render a little image for them. So let's do DiceBearAvatar, which we have to import.
So let's just go ahead and quickly import it. I'm going to just add after above this scrolls from workspace UI components, Dice Bear avatar. Let's go ahead and give it image URL of to be logo.svg like that and seed assistant and size will be 32. So now if you refresh here and go ahead and just open a chat. There we go.
You will see that the logo of your app will be displayed here. But how will this look for the users? Well, you don't have to pass image URL, right? If we don't have anything to pass, we're just going to generate a random glass of them. And how will the badge look like?
Well, you can pass the badge image URL to be logo SVG, and you will see that then they're going to have a small little badge at the bottom here, right? And maybe this is a cool idea for you to differentiate between your AI and your operators. So operators can have glass image avatar, but when it's a bot talking, it can have like a little image at the bottom. So whatever combination you want, I'm going to choose this one. And yes, we are only displaying it for a system because it makes no sense for you to display your own little avatar here it's just taking up space great so that's what I wanted was to do here we also fixed an important bug inside of our widget out screen.
So we fix this, this, this and this. And now let's go ahead and deploy this to GitHub. So I'm just going to go ahead and add all of this. This was 15 infinite scroll. Let's commit.
Let me go ahead and open a new branch, 15 infinite scroll, and let me publish the branch. And as always, let's go ahead and let's open this pull request and review it, even though this one will be pretty quick, simply because we didn't do anything important, just a few components to help us display the app better. And here we have the CodeRabbit summary to end the chapter. So we added infinite scrolling to the chat message list, allowing users to load more messages as they scroll. We introduced a new avatar component that displays a consistent assistant avatar next to relevant messages.
We also added a UI trigger for loading additional content in infinite scroll contexts. And in here we have the diagram explaining how it happens. So when the user scrolls to the top of the message list in the widget chat screen, the top element ref intersects the viewport inside of the use infinite scroll hook. That hook calls load more callback, which then retrieves new messages and returns it basically. Excellent.
So in here, it looks like we left out image URL prop inside of avatar source. So that's something we have to fix. I'm going to do that in the next chapter, but for now we can go ahead and merge this pull request. Amazing job. So this, it doesn't seem too useful what we did just now, but we're going to reuse these components in several places coming along.
So that's why I wanted to have this chapter to do that. So we will come and use these components again and you will see how useful they are. Amazing job and see you in the next chapter. And of course after you do this make sure You go inside of your main and synchronize the changes. Almost forgot that don't want you to continue building something in an older branch but even if you do no problem just merge all of those changes together.
Great amazing job and see you in the next chapter.