In this chapter, we're going to add transcript and chat to our completed state. So let's start by adding the getTranscript procedure. Confirm that you are on your main default branch, and let's go inside of our meetings procedures. So right here at the top of the page I'm going to add a new get transcript procedure. So this procedure will be a protected procedure which accepts the ID of the meeting we are trying to get the transcript from.
Now inside of here, let's go ahead and let's get the existing meeting using the input ID and ensuring that the currently logged in user from the context has access for this meeting. In case we don't have access to the meeting let's throw an error meeting not found. Now in case there is no transcript URL let's just return an empty array. And now what we have to do is we have to import JSONL from JSONLParseStringify. Now let's go down here and let's actually parse our transcript from the URL.
So our transcript is await, fetch, existing meeting, transcript URL. We return it into text, and then we use JSONL parse. And let's import our type, stream transcription item, which we defined in our ingest job. Otherwise if there are any errors such as this no longer being available we will just return an empty array. Now from here let's populate the speakers by creating a set of unique speaker IDs.
After that let's go ahead and let's create the user speakers, which means we have to import the user from database schema and in array from drizzle ORM. Make sure you have added both of them. So in here I have user from database schema. We also use the generate avatar URI in case we cannot find the user image. But we already use it in generate token, so that's nothing new.
Now, after we add user speakers, let's do the same thing to add agent speakers. But this time we are searching from agents in array agents ID, and we are using bots neutral. We already have the agents imported, but make sure that you have the agents from database schema. So now we have the user speakers and the agent speakers. So what we can do is we can combine all of them in a one single array called speakers.
And then let's go ahead and let's open transcript with speakers constant. So transcript with speakers, transcript.map and then individual item here will be used to find a speaker using this speaker ID and item speaker underscore ID. In case we cannot find a speaker we will do the same thing we did in the ingest. So if there is no speaker, we return this existing item here, and we are simply adding the user with name unknown and an image unknown, like that. But in a case that we do have a speaker, we are simply going to go ahead and return the item with that user right here.
And then finally, we can just return the transcript with speakers for this procedure. Now let's build the transcript component. In order to do that we first have to install react highlight words. Once you have installed it, let's go ahead inside of our completed state here and let's find our tabs content here and let's just add a new tabs content with the value of transcript and inside of here add transcript with a meeting ID of data meeting data ID. Now let's go ahead inside of the components and let's create transcript.esx.
And in here let's go ahead and let's import useState, format, and search icon from Lucid React. Let's import highlighter from React highlight words. And looks like we also need to import the types for this. So let's go ahead and do npm install the types react highlight words. And now you should no longer have the error here.
Then let's import use query from 10 stack react query. Let's import use the RPC from the RPC client. Let's import input. Let's import scroll area. And let's import the avatar and avatar image.
And we can also import generate avatar URI from lib avatar. Now that we have all the imports, let's create the props with a single meeting ID inside. And let's go ahead and assign the props to our con transcript component. In here, let's prepare our TRPC. And then let's go ahead and structure the data from use query and pass in TRPC dot meetings dot get transcript.
And let's do query options in here as well. And pass in ID meeting ID like that. So now we have our transcript populated with users inside. Now let's add our search query here, like that. And let's add our filtered data here.
The only thing we'll have to look out for is for the fact that data could be an empty array, so let's just do this. And now let's build the actual component here so let's return and let's add a div with our background and our padding so background white rounded large border some padding flex flex column and some gap between the items inside. Let's give it a paragraph with text mall and font medium of transcript. And then in here we're going to add our input with the search icon. So this is what this looks like.
A div with class name relative, our input component with placeholder search transcript, class name with relevant padding and a maximum width, value using the search query from above, onChange changing that search query, and a search icon with an absolute positioning because we it's inside of a relative container with left to positioning that's why we give this padding left and top meaning in the middle of the y-axis same with the translate we give it a size and text muted foreground. So let's go inside of the completed state and let's actually import the transcript because that's the best way to see how it looks. So when I click on the transcript you can see that input. Now below the input let's go ahead and let's add a scroll area outside of this div. So we add a scroll area here and let's add a div with some minimal spacing here.
Inside of this div let's go ahead and let's actually iterate over our filtered data. So we filter the data using the search query. Now inside of this filtered data, let's go ahead and let's return the following. I want to return a div with a key of item start timestamp and class name of flex flex call gap y2 hover BG muted padding for rounded MD and border so let's just close that div here. Now inside of here I'm going to open another div with vertical spacing here so flex gap x2 and center and it looks like I have some issues here So let's just see what am I doing wrong here.
I think I'm missing one more curly bracket here. So don't do the same mistake as I did. Now inside of here, let's go ahead and let's render the avatar component with class name size six. And inside the avatar image which is a self-closing component without user avatar and let's give it a source of item.user.image or generate avatar URI and pass in seed to be item user name like that. And let's go ahead and pass in the variant initials.
Let me just fix this. There we go. Perfect. So now we can already see the transcript beginning to form. Now after the avatar here, go ahead and add a paragraph rendering the user actual name like this and let's go ahead and give it a class name of text small and font medium and below this let's go ahead and let's add a paragraph which will simply format using DateFns, right?
It will simply format items start timestamp in a proper format and this is actually important. So make sure you add new date like this. And Let's give this a class name of TextSmall, TextBlue500, and FontMedium. So now you will see the actual timestamp, right? And let's just save this, and let's just refresh for this to have the proper color.
There we go. And to wrap it all up, after this paragraph, after this div, add a highlighter component, which we installed in the beginning of the chapter. Give it the following class name, the following highlight class name, search words for the search query that the user enters and filters by, auto escape true, and text to highlight item dot text. And now, for example, I can search for math and it will highlight math. I can search for plus and it will highlight plus.
And I can search for the number two and it will both filter and highlight. Now it's time to build the Ask.AI chat functionality. In order to do that, let's start by adding npm install stream-chat. Once you've done that, let's create the lib for that. So go inside of source, lib, and let's create stream-chat.ts.
And go ahead and import server only, stream chat, and then stream chat get instance, and these two variables. Now let's go ahead and let's prepare these variables here. So in here, I already have these two. And now I will just add the new ones here regarding stream. And the thing is, they are exactly the same.
So if you go inside of your stream chat messaging overview, app access key, you can see the webhook is the same for chat messaging as it is for video and audio. So let's copy the key here anyway, next public stream chat API key, and the secret is the same. There we go. I just like to keep them semantically in different variables. Now that we have our stream chat and you have confirmed the variables don't have a typo, let's go inside of our meetings procedures.
And let's go to the top here and I'm just going to add generate chat token here and that will be a protected procedure like this protected procedure mutation and we're just going to import stream chat from lib stream chat and create the token based on complex al user ID And we are then going to upsert the user and give it a role of admin and simply return that token back. So a very simple generate chat token procedure similar to our existing generate token procedure but for the video there it is it's actually this one is even shorter right and just make sure you're using your new stream chat library here Now let's go inside of the meetings UI components here and create chat-provider.tsx component. Let's go ahead and let's import UseClient, AuthClient, and LoadingState. And create an interface, props, meeting ID, and meeting name. Export the constant here, chat provider.
And the first thing you're going to do is you are going to get the currently logged in user here using AuthClient useSession. In case we are pending, we're going to return the loading state, basically telling them that we are loading the chat. And then in here, we are going to return the chat UI which we don't yet have so just return chat UI with the following props and we are now going to create the chat UI component now let's go inside of components and create chat UI dot TSX and let's go ahead and let's import use state and use effect use mutation and channel as stream channel from stream chat and then let's go ahead and let's install a package called a stream chat react so npm install stream chat react let's wait a second and once it is installed let's go ahead and let's import everything here use create chat client chat channel message input message list thread and window Then you can also add use TRPC, loading state and finally the styles from stream chat react. Now let's create the props for this component which we already saw right here so it's going to be all of those meeting id meeting name user id username and user image.
Now let's go ahead and let's prepare the component. So chat UI, destructuring all of those props above. Inside of the component here, let's prepare the TRPC and then let's destructure mutate async to be generate chat token here, use mutation and pass in TRPC meetings generate chat token and simply pass the mutation options here like that. Then let's go ahead and let's create the state channel set channel with the type of stream channel which we imported here channel as stream channel and then let's go ahead and let's actually set up our client so the client will do use create chat client and it will use our new environment key next public stream chat api key make sure there are no typos and let's use the generate chat token function right here Let's just fix this by changing this to be string or undefined. There we go, that seems to fix it here.
Great. And now that we have the client, let's go ahead and let's create a use effect which will actually connect us to the chat. So useEffect which checks if we have this client and if we don't we return and then inside of here let's do channel client channel messaging message id name meeting name and members user ID and call set channel to be channel because we defined it right here and looks like name is not accepted so I will just remove it and it looks like it's working without the name we don't actually need the name I just assumed that we will need that field. But looks like everything is fine like this. So just go ahead and add this properties right here.
And then if there is no client initialized, we're going to return the loading state with the title loading chat. There we go. So quite simple, even simpler than our video call initialization here. And now let's actually return the chat interface. So let's start with a div, which will hold the background white, rounded large border and overflow hidden.
And then in here, let's add the chat with client being client, the channel with channel being channel, the window. And then Let's go ahead and encapsulate our message list inside of a div which has flex1, overflow auto, and maximum height defined here using this calculation and border bottom. And below this, let's go ahead And let's add message input. And let's also add a thread down here. Now, let's go ahead and import chat UI from .forward slash chat UI.
Now in here, we have this error here. So let's just do this. There we go. User images string undefined. So if there's no image we'll just pass an empty string here.
And now let's go inside of our meeting ID view instead of the completed state here. And now we have to add tab content here for chat value. And let's go ahead and add the chat provider inside. So import chat provider from .forward slash chat provider. Make sure you use our component here and passing the meeting ID and the meeting name.
So now if you try clicking on here, it should be able to load the chat. Now you can try chatting here, but no one is going to answer so that's what we have to develop now. First of all, let's go ahead and let's add npm install openai so we will be using this package directly. Once you have it installed go back inside of our webhook And let's go ahead and add some new imports here. So we are going to add OpenAI from OpenAI.
And we are going to add chat completion message param from OpenAI resources index.mjs. It's basically just a type. Now let's add message new event to our list of StreamIO node SDK events here. And let's also import generate avatar URI from lib avatar. Now let's go ahead and let's create the new open AI client using new open AI.
Now that we have this, I think you don't need to define the key inside at all, I think by default, the API key will be the one that we have defined, which is OpenAI API key. But if you want to, you can explicitly add it like this. Now let's go down here to our last event, this one, call recording ready. And let's add else if event type is message.new. So just make sure that inside of your stream chat console, in chat messaging overview, you are looking for that here, right?
So just make sure that you are listening to these things. It's message, there we go. Great, so now inside of here, let's get our event. And now let's destructure everything we can from this event. User ID, channel ID, and text.
Now in case any of the three are missing, we're going to return an error because we need all of them to generate what we need. Let's start by trying to get the existing meeting. So existing meeting from meetings where we have the matching channel ID because that's technically the same thing channel ID and meeting ID and where the status is completed. As always, if we can't find that, let's immediately throw an error. And then what we need to do is we need to find an existing agent using this existing meeting agent ID.
And same thing is true if we can't find that agent, we can't proceed forward. So let's just break this webhook. Now let's check if the message that this user sent is not an agent. So this basically means if someone other than an agent sent a message in the chat, we have to go ahead and respond. And we can only do that with some instructions.
So I created a system prompt here in my public assets. My apologies, not the system prompt. Chat instructions. So go ahead and copy that and just paste it here. So instructions use the existing meeting summary and existing agent instructions to know how to behave and how to respond.
It doesn't make sense to watch me write all of that. And now below that, let's add Stream Chat, which you can import from lib Stream Chat. So just make sure you have added this import here. And let's watch the channel. And then in here, let's get the previous five messages.
So we can do that by adding channel state messages and slice by minus five filter the message by text and trim the text so we are only filtering we are only adding the messages which actually have some content inside, right? And then we are using the chat completion message param here from OpenAI resources, and we are assigning the role to be message user ID being equal to existing agent ID. In that case, the assistant sent that, otherwise the user sent it, and in here a message text with a fallback. And now we can finally generate the GPT response. So GPT response uses our OpenAI client, chat, completions API, and it adds the initial instructions to the system here, which we defined in this very big variable here.
Then, spreads the last five messages so it has some context with an idea of who said what, whether that was an assistant or a user, and then the user's newest message, and it uses this model here. And then finally, we can get the GPT response text here from GPT response choices first in the array message.content In case we are unable to extract the text from here, so if this wasn't found we're just going to return an error. And then let's go ahead and let's assign the bot avatar using existing agent name and generate avatar URI. Let's use stream chat to update that user. So upstart user, existing agent, existing agent name and avatar URL.
And finally let's send the message. So channel send message, text is the new GPT response text, and the user that sent it is right here. So make sure you have your webhook running, make sure you have npm run dev running. And if we've done everything correctly, I will refresh this. And for example, let's go inside of the summary, and let's ask, what did John Doe ask?
And let's see if we are going to get a response or not. John Doe asked, what's one plus one during the meeting? Exactly. Amazing, amazing job. You just implemented every single thing here in the summary.
So I'm going to go ahead now, and I'm going to directly commit these changes here. Actually, no, I'm going to create a new branch. So let's go ahead and create a branch. 26 transcript and chat, like this. Let's go ahead and stage all changes.
Let's add a commit message, 26 transcript and chat, and let's commit, and let's publish the branch. And what I'm going to do, just to save some time since we are at the end of the tutorial, I'm going to immediately merge this. So let's go ahead and just immediately merge the pull request. And once we have merged it, we can go back inside of main and we can click on synchronize changes. Okay.
And any second now in the graph, we should see our new pull request being merged right here. Amazing, amazing job. And see you in the next chapter.