[{"data":1,"prerenderedAt":437},["ShallowReactive",2],{"footer-primary":3,"footer-secondary":93,"footer-description":119,"100-apps-100-hours-intercom-messenger":121,"100-apps-100-hours-intercom-messenger-next":168,"sales-reps":185},{"items":4},[5,29,49,69],{"id":6,"title":7,"url":8,"page":8,"children":9},"522e608a-77b0-4333-820d-d4f44be2ade1","Solutions",null,[10,15,20,25],{"id":11,"title":12,"url":8,"page":13},"fcafe85a-a798-4710-9e7a-776fe413aae5","Headless CMS",{"permalink":14},"/solutions/headless-cms",{"id":16,"title":17,"url":8,"page":18},"79972923-93cf-4777-9e32-5c9b0315fc10","Backend-as-a-Service",{"permalink":19},"/solutions/backend-as-a-service",{"id":21,"title":22,"url":8,"page":23},"0fa8d0c1-7b64-4f6f-939d-d7fdb99fc407","Product Information",{"permalink":24},"/solutions/product-information-management",{"id":26,"title":27,"url":28,"page":8},"63946d54-6052-4780-8ff4-91f5a9931dcc","100+ Things to Build","https://directus.io/blog/100-tools-apps-and-platforms-you-can-build-with-directus",{"id":30,"title":31,"url":8,"page":8,"children":32},"8ab4f9b1-f3e2-44d6-919b-011d91fe072f","Resources",[33,37,41,45],{"id":34,"title":35,"url":36,"page":8},"f951fb84-8777-4b84-9e91-996fe9d25483","Documentation","https://docs.directus.io",{"id":38,"title":39,"url":40,"page":8},"366febc7-a538-4c08-a326-e6204957f1e3","Guides","https://docs.directus.io/guides/",{"id":42,"title":43,"url":44,"page":8},"aeb9128e-1c5f-417f-863c-2449416433cd","Community","https://directus.chat",{"id":46,"title":47,"url":48,"page":8},"da1c2ed8-0a77-49b0-a903-49c56cb07de5","Release Notes","https://github.com/directus/directus/releases",{"id":50,"title":51,"url":8,"page":8,"children":52},"d61fae8c-7502-494a-822f-19ecff3d0256","Support",[53,57,61,65],{"id":54,"title":55,"url":56,"page":8},"8c43c781-7ebd-475f-a931-747e293c0a88","Issue Tracker","https://github.com/directus/directus/issues",{"id":58,"title":59,"url":60,"page":8},"d77bb78e-cf7b-4e01-932a-514414ba49d3","Feature Requests","https://github.com/directus/directus/discussions?discussions_q=is:open+sort:top",{"id":62,"title":63,"url":64,"page":8},"4346be2b-2c53-476e-b53b-becacec626a6","Community Chat","https://discord.com/channels/725371605378924594/741317677397704757",{"id":66,"title":67,"url":68,"page":8},"26c115d2-49f7-4edc-935e-d37d427fb89d","Cloud Dashboard","https://directus.cloud",{"id":70,"title":71,"url":8,"page":8,"children":72},"49141403-4f20-44ac-8453-25ace1265812","Organization",[73,78,84,88],{"id":74,"title":75,"url":76,"page":77},"1f36ea92-8a5e-47c8-914c-9822a8b9538a","About","/about",{"permalink":76},{"id":79,"title":80,"url":81,"page":82},"b84bf525-5471-4b14-a93c-225f6c386005","Careers","#",{"permalink":83},"/careers",{"id":85,"title":86,"url":87,"page":8},"86aabc3a-433d-434b-9efa-ad1d34be0a34","Brand Assets","https://drive.google.com/drive/folders/1lBOTba4RaA5ikqOn8Ewo4RYzD0XcymG9?usp=sharing",{"id":89,"title":90,"url":8,"page":91},"8d2fa1e3-198e-4405-81e1-2ceb858bc237","Contact",{"permalink":92},"/contact",{"items":94},[95,101,107,113],{"id":96,"title":97,"url":8,"page":98,"children":100},"8a1b7bfa-429d-4ffc-a650-2a5fdcf356da","Cloud Policies",{"permalink":99},"/cloud-policies",[],{"id":102,"title":103,"url":81,"page":104,"children":106},"bea848ef-828f-4306-8017-6b00ec5d4a0c","License",{"permalink":105},"/bsl",[],{"id":108,"title":109,"url":81,"page":110,"children":112},"4e914f47-4bee-42b7-b445-3119ee4196ef","Terms",{"permalink":111},"/terms",[],{"id":114,"title":115,"url":81,"page":116,"children":118},"ea69eda6-d317-4981-8421-fcabb1826bfd","Privacy",{"permalink":117},"/privacy",[],{"description":120},"\u003Cp>A composable backend to build your Headless CMS, BaaS, and more.&nbsp;\u003C/p>",{"id":122,"slug":123,"vimeo_id":124,"description":125,"tile":126,"length":127,"resources":8,"people":8,"episode_number":128,"published":129,"title":130,"video_transcript_html":131,"video_transcript_text":132,"content":8,"status":133,"episode_people":134,"recommendations":149,"season":150,"seo":167},"b8b36125-7a4a-40e4-85f6-f4fe9138085e","intercom-messenger","954528032","It's a race to a real-time chat widget in this episode. Bryant tries to build his own version of the Intercom Messenger - a business messaging widget that includes live chat, helpful articles, and other rich content.","28671146-5bfb-411f-9bea-a57589a931a0",64,8,"2024-06-07","MIssion: Intercom Messenger Clone","\u003Cp>Speaker 0: Alright. Welcome back to yet another episode of 100 Apps, 100 Hours. I'm your host, Brian Gillespie, developer advocate here at Directus. And if you're new to the show, the basic gist is this, we take 60 minutes and we try to build or rebuild some of your favorite apps or some of your wildest app ideas, in that 60 minutes, we achieve our stated goals or we publicly fail trying. And by we, I mean me.\u003C/p>\u003Cp>That's okay though. When I fail, I fail spectacularly, usually. But I've got 3 little girls so I'm used to being hobbled. That's okay. There are just two rules.\u003C/p>\u003Cp>Number 1, you have 60 minutes to plan and build, which does pose some challenges. There's no more, no less. And number 2 is you use whatever you have at your disposal, just like in the real world. You know, make it work. Figure it out.\u003C/p>\u003Cp>And in today's episode, we are going to take advantage of rule number 2. So what are we building today? We are working on the intercom Messenger. Right? Intercom is a, like, help desk, onboarding tool, messaging tool.\u003C/p>\u003Cp>They've got all these things that are wrapped up into it. It looks like their latest positioning is, like, customer service, which is great. But like most of their it comes from this little messenger widget over here. Right? So it's not just live chat.\u003C/p>\u003Cp>It is kind of like a rich format where they can check product news, they can chat with the support team, there's bots, there's AI, there are articles integrated, you know, there's, like, product tours and stuff like that. The the main things that we're concerned with here are this kind of nice interface for this, and then number 2, like this actual, like, live chat functionality and maybe the ability to add articles. So we keep the user in context, That's what we're gonna be tackling today. I I think it's doable, not a 100% sure. Let's go in and start the clock on this.\u003C/p>\u003Cp>We will start our timer, 60 minutes and away we go. Alright. So as far as what type of functionality we want out of this, you know, what are the the jobs to be done or what are the goals for this specific project. We want to be able to chat chat with visitors in real time and then, you know, maybe display rich content inside the messenger. This could take the form of articles, help center stuff, updates, etcetera, and I totally cannot spell.\u003C/p>\u003Cp>How are we gonna get there though? Right? Normally, if you've watched any of the past episodes, at this point, I'm like doing some data modeling. The starting point for today is a little different in that I've already got a project that we're going to lean on. It's called Agency OS and it already has a a fully fleshed out data model.\u003C/p>\u003Cp>You can explore the front end of this at agencyos.dev. But this actual project you can download and test out yourself. It lives in our Directus Labs organization at, Directus dash labs slash agency dash os on GitHub. Basically, there's some instructions, but the main features of this include a website, CRM project tracker type of thing, and then you have a client portal. So on the front end, it kinda looks like this, your standard agency website.\u003C/p>\u003Cp>You know, we've got all these different blocks. It uses the page builder inside Directus. We got blog posts and articles. You know, there's a a help center that should be displaying some articles that doesn't. And then you have, this client portal where clients can log in, keep track of their different projects, see files, see their invoices, pay for those specific invoices.\u003C/p>\u003Cp>Right? This is this is pretty neat functionality. And then, you know, you would basically manage your entire workflow from your deals all the way through, like proposals, projects, and tasks inside your direct assistance. Right? So I can see a list of all the tasks that are necessary to complete this specific project, who's the organization, who are our contacts for that organization, And I can manage this whole thing here.\u003C/p>\u003Cp>But in this case, I'm using this because we've already got this conversations model and this messages collection. And we have things like help center collections and help center articles already mapped out, which is is really nice, just to kind of piggyback off of what I've already built in the past. So if we dive into conversations, you know, those are gonna be I can have multiple conversations ongoing at once. Those would be like the different threads. Inside that we've got like a user that created it.\u003C/p>\u003Cp>Maybe a visitor ID in this case, if the visitor is not known. We've got some messages inside there. Inside the messages, each message has a bit of text, who is the user or the visitor ID that created it, and away we go. That's what it looks like. We're gonna dive into like some of the real time features of Directus as well.\u003C/p>\u003Cp>So what have I actually got set up for you here? In starting this project, I've got a Nuxt application that I often use as a starter here for this. You know, the Nuxt application just has a direct us client using the SDK already preconfigured with, authentication and rest. I've got some login and register pages, so we can kinda simulate that. But that's kind of it.\u003C/p>\u003Cp>The other like 2 minutes worth of setup that I have done, this Agency OS project, before the V2 release, I had a janky little chat widget that I was trying to use in this specific project. So I can go back in time here and find this little chat widget. Let's just copy this in. And, it looks like I've already got it copied in, and cleaned up. But just a little chat box that we could use.\u003C/p>\u003Cp>And, I could never quite get this to work before the big release, so, it it ended up getting scrapped. So let's revisit this. I've got our chat widget. Let's drop this thing into our index page. So if we open up this incognito window so I don't spoil any of the, so I'm not using that direct to session token basically.\u003C/p>\u003Cp>Alright, so I've got my page here, I'm just going to drop this chat widget in there. And now I've just got this shell for a chat widget. It opens, it closes, it kinda looks nice. That's really all it does, right? There's this motionable component that uses the view motion View Use Motion, composables.\u003C/p>\u003Cp>Where is it? View Use Motion at view use slash motion. Just to get like some smooth animations using, I think it uses pop motion the hood. And it's just, tailwind for styling, right? Our content is gonna go in here.\u003C/p>\u003Cp>But let's fulfill that first thing, right? We wanna be able to chat with visitors in real time. I don't even know why these are bold, that's gonna aggravate me. Alright, how do we chat with visitors in real time? Directus has powerful real time functionality built in.\u003C/p>\u003Cp>Now inside my Docker Compose file for this, I've already got real time enabled. So that is the WebSockets enabled part here. And when you're working with real time and direct us, there's 3 authentication modes. There's public, handshake, and strict. Public just means anybody can open a connection.\u003C/p>\u003Cp>Handshake means that you don't have to provide the authentication as the you could still connect but the first message has to be authentication. And then strict means you have to pass the authentication before you actually initiate a connection. Alright, so what we're wanting to do here basically, we're gonna have to extend our plugin that we have. So I'm gonna go into my Directus plugin that I've got. And again, it's just creating a simple SDK client to work with.\u003C/p>\u003Cp>I'm gonna go in here and let's actually create a second client. Create Directus Real Time Client. So I'm just gonna create a new Directus client and I'll just copy paste this previous client. Great. Except with, we're not gonna use any of this, we're gonna use the real time composable.\u003C/p>\u003Cp>Alright? And there is an auth mode property in this case and I'm just gonna pass public because we want anybody to be able to connect to this. So I'm gonna create that client and then the next thing that I'm gonna do is provide that client to you, the Nuxt application. Let's just call it directus sws for directus web socket. And we're gonna pass directus ws.\u003C/p>\u003Cp>Okay. So now we have created this client. Our chat widget still doesn't do anything. That's fine. What we're gonna do is open up chat widget and we're going to access that client.\u003C/p>\u003Cp>So we'll do directus ws, because Nuxt provide when you use Nuxt provide it adds a dollar sign to the front of it. And we're just going to do use Nuxt app composable. That will give us access to our web socket client. And then we're going to actually open a connection, right. And this application is using like server side rendering inside Nuxt.\u003C/p>\u003Cp>So I probably want to like wrap this up inside a composable as well like the like an on mounted hook. So we'll await the Directus WebSocket connect. And I I don't work with Directus real time a lot, so we may get some funky behavior here that needs to be async. Okay. So I'm just gonna open up the console and see if I open up all and I go to web socket, we can show a connection here.\u003C/p>\u003Cp>It says switching protocols. Okay, AWS local host. Alright, so looks like we've got the WebSocket connection opened. Alright, now let's just kind of dig into the documentation a little bit. Because I the next thing is gonna be, like, subscribing, but we probably need some message handlers.\u003C/p>\u003Cp>So we'll go into get started with real time on the documentation. I am doing this, just as you might. So you can see that we've got the real time client here. We've opened a connection to the client, or at least I think we have. And then now we've got like this client.\u003C/p>\u003Cp>We need to listen for changes. Right? So I might as well just plop this in our on mounted hook. And I'm just gonna replace client dot with dollar sign directus ws. And now let's see what we actually get out of this.\u003C/p>\u003Cp>Do we see any actual messages here? I don't know why am I not seeing any messages. Directus cannot connect when state is open. Do we need to do we actually need to pass the auth mode to this? Is that an actual property?\u003C/p>\u003Cp>Switching protocols, directus.ws.subscribe. Let's subscribe to messages and see what happens. On message data dot undefined. Message. What if we just console log the message here?\u003C/p>\u003Cp>Console log message. Alright. Alright. So we see we're not getting anything there. Subscribe error, invalid collection.\u003C/p>\u003Cp>Okay. So not getting the correct collection. Let's just take a look at this. This is messages. Oh, let's try subscribing to messages, right?\u003C/p>\u003Cp>On open console log event open. I don't know why it is not not sure why it's not working correctly. You do not have permission to access this. Right? So as a public user, we don't have access to the individual messages.\u003C/p>\u003Cp>Alright, so two ways around this. We could potentially provide some authentication, if I can actually talk, right, or we could give access to this. Alright. So if I go into my access control settings, I'm gonna go to public in this case, and I'm not gonna give all access, but maybe we use custom. Right?\u003C/p>\u003Cp>2 things here, we get the public can create messages, that's fine. But when it comes to reading messages, we don't want people to be able to read other people's conversations. So in this case, let's add just a rule to it. If the conversation, conversation visitor ID contains what? Can we even do that, right?\u003C/p>\u003Cp>We'd have to pass the let's do this instead. We'll use our website API user for this. And okay. We'll do messages. You can see conversations.\u003C/p>\u003Cp>I tell you what, let's just open the floodgates here just to test this out and make sure we can see these things. Alright, so we've got a subscription. There's our data, okay. And if I open this up and I add a new message, right, which is the test message, save this, we should see that data come through, which we do over here. Right?\u003C/p>\u003Cp>So we see our subscription for the messages. Do we see our actual data here? Great. There's our text, test message. Here's the user that created that.\u003C/p>\u003Cp>That's gonna be me, the admin user. And we've got the date created. Right? We've got all the details of this specific message. Alright.\u003C/p>\u003Cp>So at least now we are getting the messages. Alright, if we go back to the documentation, we're getting some messages here. That's great. You know, a couple things when we create a subscription, right? We can choose what event to subscribe to.\u003C/p>\u003Cp>So the event wise, we'll probably do like create. And we can even add a query, right. So imagine we've got a website visitor. We want to basically, like, create a unique session or a unique ID for that visitor, so that that user can only see their messages. Right?\u003C/p>\u003Cp>So what do we need to do there? This might be something like a Nuxt Middleware. Do I have do I have UUIDs in this? Okay. So I've just got like a function in here and and this is, like a lot of this is actually ripped from the AgisUS project to begin with.\u003C/p>\u003Cp>But I've just got a function here for generating an ID, which basically just creates like a UUIDv4. Alright, so what I'm gonna do here, let's just create like a session dot global dot ts. I'm gonna add this in the middleware folder inside Nuxt. And when I do this, if I use dot global that will run on every route. So I'm just gonna go here, I'm gonna copy this, and I don't really need to here.\u003C/p>\u003Cp>Basically, anytime we navigate to a route, we wanna do some things. Right. We want to, let's check like a session ID or visitor ID. Visitor ID equals use cookie so we can set a cookie that way we can access it server and client side. We got visitor ID.\u003C/p>\u003Cp>If visitor ID dot value, if there's no visitor ID dot value, we are going to set up a where is it? I'm gonna do Visitor.id.value equals generate ID. There. Okay. Alright so now basically we just created a middleware that's gonna run globally on every route within this application and it will create a cookie for the visitor ID.\u003C/p>\u003Cp>Okay. And now I can see that here that I've got a visitor ID cookie which is some random UUID value. If I delete that, refresh, we can see we get a new visitor ID. But that cookie will persist across sessions, which is nice, and we don't have to do anything else there. Okay, so as far as our query, if we just go back to the documentation, right, we can add a query, and we can do all of the same query params that we would in the regular Directus API.\u003C/p>\u003Cp>So we do something like filter where, well actually let's back up a minute, we need to get that cookie, right? So visitor ID is equal to use cookie visitor ID. Okay. And then for our filter, we want the messages. Let's think about this, right?\u003C/p>\u003Cp>We're gonna have to create a conversation. Let's just get some messages first, right? So we're gonna add the visitor ID to each message. Alright. So maybe we just wanna be able to see we're gonna have to like create a conversation.\u003C/p>\u003Cp>There's no way getting around that. Alright. How can we create items, right? So we can use that same connection and then we can actually get all of our like CRUD operations. So let's just do this.\u003C/p>\u003Cp>We're gonna actually set this up. I wanna subscribe to 2 items, right? Conversations. And in this case, we want to filter where the visitor ID is visitor.id.value. So inside our data model, right, when we create a conversation there's a visitor ID for it so we could track that.\u003C/p>\u003Cp>Great. Okay. So I could see that there. Then we have the individual messages, and we we've honestly got a lot of extra stuff that we're probably not even really gonna need here. But we got visitor.\u003C/p>\u003Cp>Id_id, that's what it is inside our database. And then we've got the cookie value here. So visitor dot ID, and then the param is gonna be like this, equals to visor.id.value. And then I think we can also pass a UID to this subscription. Let's look at it.\u003C/p>\u003Cp>Learn more about authentication, learn more about subscriptions with WebSockets. Alright. Using UID's type is subscribe multiple subscription UID. Okay. So we'll call this UID And this will be visitor conversations.\u003C/p>\u003Cp>Thank you, GitHub Copilot. Let's also open a connection for messages as well. Subscribe messages where messages dot create, query, UID. And in this case we can't just filter out for the visitor messages, right? We also have to be able to show the admin messages as well.\u003C/p>\u003Cp>So in this case what we're gonna do, we're gonna work on the conversations because we can use the related fields inside direct us collection. So I can go up a level here. It's actually conversation. Is what's the parameter gonna be? Right?\u003C/p>\u003Cp>So we could have potentially multiple conversations. Right? So we'll have conversations equals ref, and then we'll have messages. Actually, we might just store all of those. Right?\u003C/p>\u003Cp>Conversation, let's look for our query params, global query parameters for our actual filter here, filter rules, we're looking for items within an array. So we're going to use n, in conversations dot value map conversation dot ID. Okay. I think this should give us what we want. And if I open this now we can see that we've got a subscription for conversations, we've got a subscription for messages.\u003C/p>\u003Cp>I'm gonna just copy the visitor ID here. And the other thing I'm gonna do, I'm gonna create a new conversation. So we'll say new conversation. I'm gonna add that visitor ID here. We'll call this test convo.\u003C/p>\u003Cp>And do we get a message for that? We can see this conversation here. And now let's just test this as well. Will we get, TestConvo not from visitor? Just want to make sure.\u003C/p>\u003Cp>Okay. So you can see I created a new conversation that didn't have that user ID or that visitor ID. And now we don't actually see that. So how are we doing on time? We got about 35 minutes left.\u003C/p>\u003Cp>We're looking okay. So let's start having some conversations. Right? On messages, on init, Subscription init. Get conversations.\u003C/p>\u003Cp>Alright, subscribe on open on message. Okay, so basically all we're doing at this point is listening for messages. And just logging that out, right? So we need to do some other things here. Is there like a is there another property on a knit?\u003C/p>\u003Cp>No, a knit. I need more practice with real time, obviously. Alright. So when we receive the message, let's look and see what our references are here. Full code sample, I think there's actually a guide that I built on multi user chat or somebody built this.\u003C/p>\u003Cp>Alright. Connect cleanup, Subscribe. Okay. So you can get the actual subscription equals await client dot subscribe. Constant subscription equals ws await client.subscribe.\u003C/p>\u003Cp>And then we can get our initial messages. So here, let's just call this get initial combos. And we might also want to grab all the fields within that as well. So we're gonna get all the root level fields for the conversation, and then we're also gonna get all the message values included in that. And then let's populate conversations.value=subscription.data.\u003C/p>\u003Cp>I'm not even sure it's gonna be that. We'll just do it's actually console log subscription. See what we get there. Alright, subscription L2 generator. Cons to subscription.\u003C/p>\u003Cp>Subscription, unsubscribe, generator state suspended. For message of subscription, for await combo of subscription push combo. Subscription is not iterable. Let's just wrap it like in the documentation here and see what we actually get. Right?\u003C/p>\u003Cp>So now if I show let's just show the actual conversations. So we'll go into our content section, and we'll do divv4conversationinconversation. Flex call. Let's just add let's just log that out in conversation. Prepping the pretag.\u003C/p>\u003Cp>Type subscription event, init. Yeah. So this might need to actually be updated. Right? Subscription event, await client subscribe.\u003C/p>\u003Cp>Subscription Subscribe to messages, create event. What am I missing here? Subscribe if type. This message is create, receive message. Subscription started.\u003C/p>\u003Cp>Okay. We're not actually getting the message history there. Display historical messages. Okay. Alright.\u003C/p>\u003Cp>So basically to get all of the historical messages, we need to read the messages. So we'll do client dot, it's actually gonna be directus dot send message, and then we'll get all of the items within the conversations. So that'll just be items. The collection will be conversations. And the action is gonna be read here.\u003C/p>\u003Cp>So we just wanna read all of the conversations. And then as far as the query, we'll set a filter where visitor ID equals visitor ID and we'll just get all of those conversations. And maybe we limit those to like the past 10 conversations, kinda like our documentation here. Alright. Send message.\u003C/p>\u003Cp>Okay. So we're gonna get that message. Send message. There's our items that we could see. There's those conversations there.\u003C/p>\u003Cp>We could see the individual messages within those, but we're not actually populating that data somewhere. Right? So we're going to need on our message function here, basically if the data type equals items, okay, so when we receive a message, we are going to take our message. Alright. If the message dot type conversations there we go.\u003C/p>\u003Cp>GitHub Copilot knows what we want. And we'll just refresh when we open the connection. If I go in and we look for chat widget inside the dev tools, do we see our conversations? We don't. Alright.\u003C/p>\u003Cp>Why not? Message. Message, message. If let's just update this as well. Is connected.\u003C/p>\u003Cp>State is not open. Okay. So we're getting our messages. They're just not populating to what we want. Right?\u003C/p>\u003Cp>So we see the data is an array. That should be what we want though. If we open this up, we should be seeing those conversations. Direct to send messages, items, conversations, if equals conversations, conversations dot value equals messages dot data. Conversations.\u003C/p>\u003Cp>Yeah. That should be right. Collection equals conversations message dot type. What is the the type equals items? Oh, we don't really have a, maybe we wanna add a UID.\u003C/p>\u003Cp>Type equal to UID equals get combos. Alright. So now if we take a look at that, right, we should have that UID get combos. And we could just target it that way. If message dotuid equals get combos, and now we populate that data.\u003C/p>\u003Cp>Okay. Alright. So now we can see a conversation there. That was a lot of stress just to get this conversation. And let's just go in, and I'm gonna add, a message to this.\u003C/p>\u003Cp>Hi. Hey. Okay. Now if I refresh, we can see the actual messages within that. That's solid.\u003C/p>\u003Cp>Okay. And then we wanna do something like this where we have selected convo equals ref. Let's just call it conversation. Okay. Solid.\u003C/p>\u003Cp>Alright. So now we need to actually like start showing our our conversations. Right? So within this, we want to maybe wrap this up. Right?\u003C/p>\u003Cp>We'll do it in like a template tag. If there is no if no selected conversation, We're gonna show this information. Then we'll come down and do another template v if selected conversation will show something else. Alright. So instead of our actual conversations here, what if we show we wanna add a button.\u003C/p>\u003Cp>Do we want to make it a button? Yeah, we do. And then we'll do like the conversation p conversation dot title, I think we've got a title for that. Alright, so we refresh, we see test convo. Let's give each one of these a lot of padding.\u003C/p>\u003Cp>Okay. So we'll do like py6px4. You know, maybe we wrap these again. Div. Do like a divide y situation.\u003C/p>\u003Cp>Okay. Test convo. Test messages dot text. And, you know, maybe we show like the user avatar as well. Alright, so again, like we might have to drill down into this a little further and do messages dot user created, user underscore created.\u003C/p>\u003Cp>And this should give us like all the actual user data in addition to that. Right? So I wanna make sure that we've got let's see if we can get, like, the user avatar source equals, use files, conversations dot messages dot user ID. Can we show the user avatar? Where does this use files to?\u003C/p>\u003Cp>File ID. Alright, let's just check the messages real quick. Chat widget, conversations, messages. Are we getting user created? We're not getting that information, so we probably need to control that with access control as well.\u003C/p>\u003Cp>Alright. Users. Again, we're just gonna open the floodgates. Would not do this on a regular app. Make sure what other rules do we have here?\u003C/p>\u003Cp>Public. Okay. And I really don't even have a I don't think the admin user has an avatar here either. So let's just add one for that. All files, just added avatar for me.\u003C/p>\u003Cp>Refresh. Are we actually getting that avatar? We're not. Anonymous component chat widgets. We'll go in.\u003C/p>\u003Cp>Here's our conversations. Here's our messages. We'll get the user created. We could see the avatar, access control public, Directus files. Let's just give all access.\u003C/p>\u003Cp>Yep. Still not showing it. Use files. Let's just call it, like, constant file. It may not even be able to use files.\u003C/p>\u003Cp>What is actually this? Get probably need to look at the actual file URL. That's the actual method here. That's why I'm not getting that correct. Alright.\u003C/p>\u003Cp>So file URL. Okay. There we go. Alright. So that's not necessarily pretty or amazing.\u003C/p>\u003Cp>Let's flex it, test convo. Maybe we'll just show like the number of messages dot length. And wrap this. And then we do, p, maybe like get I got a getRelativeTime conversation. Oops.\u003C/p>\u003Cp>Just got to wrap that. Get relative time, conversation, date created. 16 minutes ago, we'll add a little bit of gap here. And let's do text left. Okay.\u003C/p>\u003Cp>Alright, so we got the test convo and now when I click the button at click, we want to set the selected conversation to the conversation dot ID. Okay. And now we can see that goes away. And now we just need to show the messages within that, right? So selected, let's do a conversation messages.\u003C/p>\u003Cp>Yep, get up Copilot knows what I want to do. If that is has no value, we're gonna return conversation messages. Let's see. Select a conversation dot value, conversations dot ID. Okay.\u003C/p>\u003Cp>Return conversation messages. Okay. Yeah. Sometimes AI is not to be trusted. Sometimes it's okay.\u003C/p>\u003Cp>Alright, so then let's just show the actual messages, right? Do the same thing. Let's copy this down. Div. Alright.\u003C/p>\u003Cp>And then we're gonna show we'll wrap this. And then we need a div for each message. So v for message in selected conversations, key equals message ID. Let's see what this shows us. Right?\u003C/p>\u003Cp>We've got the test conversation, selected conversation messages. Alright, so I select that. We can see this. We don't have the message text. Hey, there's the message text.\u003C/p>\u003Cp>We're going to need like some padding here. Okay. Maybe we wanna wrap each message in like a let's add just a little bit of padding. We'll wrap each message in white light gray, gray 100. And give it a little bit of padding.\u003C/p>\u003Cp>P p 2, Rounded XL. Okay. So there's our messages. How are we doing on time? We got like 17 minutes.\u003C/p>\u003Cp>We are quickly running out of time, right? So now the bottom of this, we can see the messages generated from the user here. And the other thing that I wanna do here is when we send a new message, or when we receive a message, right, if this is the, so we whenever we create new messages, it needs to be we're subscribing to the messages. Alright. So here, we're gonna need to pass the messages are received.\u003C/p>\u003Cp>We wanna add those add them to the proper conversation. Alright. So what is GitHub Copilot coming up with? We got conversations dot value dot find, message dot data dot conversation. Okay.\u003C/p>\u003Cp>If conversation dot push. All right, let's just test this out and see if we're getting what we actually want here. Local host 8055. Alright. So we got our convo there.\u003C/p>\u003Cp>I could click into it and see our test message. And now, like hopefully if I go in and add another message here and just save, We should get that, but we're not seeing that message. Test combo, yo. Why are we not receiving that? Console dot log dot message, web socket messages.\u003C/p>\u003Cp>Conversation In That should be a message of the conversations. Subscribe. Is that after maybe we need to wait getting those initial messages? All right, so let's open this up. Now we can see both of those messages, right?\u003C/p>\u003Cp>I do tttt, we're still not getting that message. So let's just drop this for now. We would come back and clean this up, but I I just wanna actually get the messages. Alright. T t t.\u003C/p>\u003Cp>Test. So we're getting that visitor messages array. It's just not populating to the proper conversation. Alright. So don't trust GitHub.\u003C/p>\u003Cp>If git combo and messages what's the actual message that we received? Git convo subscription event dot create visitor messages. It's basically we need to use that UID again. UID, those visitor messages and message data. I think that should take care of it.\u003C/p>\u003Cp>Test convo. Still not getting that. Right? Chat widget. Select a conversation messages, select a conversation, conversation.\u003C/p>\u003Cp>It's just not populating those messages here. What are we doing wrong? What are we doing wrong? If conversation oh, conversation dot value? Conversations dot value dot fun.\u003C/p>\u003Cp>That's not really gonna work, is it? So we need to like we're getting the conversation value. So here's the message. Here's the data. We're getting back an array of data actually.\u003C/p>\u003Cp>Conversations dot value dot find data. Message dot data is an array. Okay. Conversation. I don't even know if this is actually gonna work, is it?\u003C/p>\u003Cp>That shouldn't work. We'll test it and see. Yeah. It's not gonna work. Promise dot avatar.\u003C/p>\u003Cp>So we need to definitely add that as well. To our query, we're gonna add the same thing or similar, where we have fields, we're gonna have user created dot dollar sign. Okay. So we got our test convo. Can we receive messages from Reading an avatar.\u003C/p>\u003Cp>Man. Okay. Conversation dot messages dot push. Why is it not seeing the avatar? Chat widget, conversations, messages within that.\u003C/p>\u003Cp>It's pushing that into the array, message.data.0. That would be why. Alright. One more time again. I'm just gonna delete some of these.\u003C/p>\u003Cp>Save it. We refresh over here. We can see there's our convo. We do yo. We save.\u003C/p>\u003Cp>Okay. So now over here on the client, I can see that conversation or that message being populated. So we'll just add a little gap between these space space y 2. Okay. And we'll probably need to do like message dot user created dot if we have a message user dot created and message dot user created dot avatar.\u003C/p>\u003Cp>Alright, so we're only gonna show that if we have it. And then the next thing that we need to do in a hurry here, about 9 minutes, is to actually start populating these messages, right? So we want to add some form fields to this down here at the bottom somewhere. So if we're inside the selected conversation, great, let's add the message form. Cool.\u003C/p>\u003Cp>Alright, and we'll do, I've got this nice Nuxt UI library here. So we'll do Submit and Message and it just gives us some nice inputs here. Flex column, there's the wrapper. Where is this actually gonna be? Inset 0, uform, send message, v model, new message.\u003C/p>\u003Cp>Alright, so let's add our new message. New message equals ref, that'll be blank. Let's have a function for send message. Okay, send messages, create conversation, select a conversation dot value, new message dot value, user created. Nope, Where did you go?\u003C/p>\u003Cp>We can rely on AI here. And we don't want a UUID or UID for this. Oh, maybe we do. Send message. And we're we're not gonna use the user created.\u003C/p>\u003Cp>Alright. Does this give us, where's my little bar at? User, User selected conversation. There's no selected conversations. Vfusermessage.created.\u003C/p>\u003Cp>Oh, message dot user created dot avatar. Okay. So now we're going to form. The form should actually be pushed to the bottom. So that'll be flex 1 height full.\u003C/p>\u003Cp>Okay. Then inside this form, I want that to flex, justify between. Okay. And we add a bit of padding to that as well, p 2. Give it a border, border t.\u003C/p>\u003Cp>And we stick it to the bottom. Nope. This will be like overflow. Why? Auto.\u003C/p>\u003Cp>Okay. Flex justify between, and this could probably even be like stuck to the bottom in the absolute. Alright. So let's just change this to like text area. Hey hey hey.\u003C/p>\u003Cp>And now we can see we can actually chat with that specific person, inside our send message, we're probably gonna delete that new message as well. Just set that value once that's actually populating. Okay. So now I can see my convos. I can open up that convo.\u003C/p>\u003Cp>I can send a message to their team. Great. How could we actually show the help center articles? Right? In 5 minutes, can we get this rich content within there?\u003C/p>\u003Cp>We're going to be pushing that, right? Pushing it quite a bit. Alright, so we got our chat widget. We're probably gonna need like a page functionality or something. Like hey, what page are we on?\u003C/p>\u003Cp>The default is, let's set that to chat. So this will be this is our conversations. And can we wrap this in another template tag? Or it might be easier just to stick it inside a div. Alright, so div if page equals chat, We're gonna show that.\u003C/p>\u003Cp>Alright, so we should still see that now when we open this up. Still see that. Great. But let's just show another page. Div v if page equals articles.\u003C/p>\u003Cp>Alright. So then we're gonna show the articles. Maybe we show like a card v 4, Article in articles. I don't think any of these are actually gonna apply. View card, we'll just do like a p tag for the article dot title And see what this shows.\u003C/p>\u003Cp>Key is equal to article dot ID. Alright. And now we need like some kind of widget at the bottom of this to actually show, right? We need like a menu. So let's do div, do you like buttons?\u003C/p>\u003Cp>This is not at all. Show an icon. Name equals herocon's chat bubble left ellipsis. Let's just call this chat. And actually, and then we need a new button for icon name equals articles.\u003C/p>\u003Cp>How are we doing on time? 3 minutes. Are we gonna actually get this or not? Let's see what this shows. Chat.\u003C/p>\u003Cp>This is gonna be absolute bottom 0, p 4flexgap4, give that a little gap. I don't particularly like the icons for this. Let's just okay. And then if click at click, this is gonna be page equals articles. There we go.\u003C/p>\u003Cp>And at click page is equal to chat. Okay. Is that actually gonna work? Page is equal to chat. Page is equal to articles.\u003C/p>\u003Cp>Okay. So now we're switching those back and forth. We just don't have any articles, right? So let's use our client here. We've got articles.\u003C/p>\u003Cp>We could do a ref. Just make that an array. And then we'll have like a function, get articles. And we can use that same connection again. Right?\u003C/p>\u003Cp>Fetch articles. Yeah. I could use the regular rest connection as well in this case. But let's try to use this. Send message.\u003C/p>\u003Cp>We want collection of articles. This is gonna be a read. And the data, UID, we'll just do get articles. Okay. Oh, how we doing?\u003C/p>\u003Cp>We got 1 minute 20 seconds. Get articles. That's our function. Okay. And then on our receive bit here, if message dot UID and message data articles dot value dot messages.\u003C/p>\u003Cp>Okay. What else do we what else do we need to do here? We need to fetch those articles first, right? So let's do like a watcher. Watch page.\u003C/p>\u003Cp>New page. If page equals articles, get articles. Is this actually gonna work? Error. Invalid collection.\u003C/p>\u003Cp>Oh, no. We don't have articles. They're what? Post. Post.\u003C/p>\u003Cp>Post. So what is this? There we go. With what time to spare? 21 seconds.\u003C/p>\u003Cp>Right? Okay. So we can actually see a list of the articles. That's about as good as we're going to get in in this amount of time left. I'm calling that a win.\u003C/p>\u003Cp>So we can actually show the list of articles here. The next part would be to just probably actually, like, set these up to to, like, be able to read the rich text for those articles right within the little widget. But, you know, that is a wrap. Great. And if I open up Directus, we can see all the messages within that conversation.\u003C/p>\u003Cp>And honestly, I would probably build some kind of interface over here inside Directus to be able to manage this. Alright. So if we just wrap this one up, did we actually get to the intercom messenger? You know what? In an hour, almost impossible to rebuild.\u003C/p>\u003Cp>I'm I'm calling that now unless you built it before. But, yeah, far cry. But hey, the basics are there. And it goes to show you just how, how far along you can get with Directus in an hour using the tools that are provided. So that's it for this episode of 100 Apps, 100 Hours.\u003C/p>\u003Cp>Hope you enjoyed following along. You could probably see the sweat bubble up on my face at this point, but stay tuned for the next one. We'll see you.\u003C/p>","Alright. Welcome back to yet another episode of 100 Apps, 100 Hours. I'm your host, Brian Gillespie, developer advocate here at Directus. And if you're new to the show, the basic gist is this, we take 60 minutes and we try to build or rebuild some of your favorite apps or some of your wildest app ideas, in that 60 minutes, we achieve our stated goals or we publicly fail trying. And by we, I mean me. That's okay though. When I fail, I fail spectacularly, usually. But I've got 3 little girls so I'm used to being hobbled. That's okay. There are just two rules. Number 1, you have 60 minutes to plan and build, which does pose some challenges. There's no more, no less. And number 2 is you use whatever you have at your disposal, just like in the real world. You know, make it work. Figure it out. And in today's episode, we are going to take advantage of rule number 2. So what are we building today? We are working on the intercom Messenger. Right? Intercom is a, like, help desk, onboarding tool, messaging tool. They've got all these things that are wrapped up into it. It looks like their latest positioning is, like, customer service, which is great. But like most of their it comes from this little messenger widget over here. Right? So it's not just live chat. It is kind of like a rich format where they can check product news, they can chat with the support team, there's bots, there's AI, there are articles integrated, you know, there's, like, product tours and stuff like that. The the main things that we're concerned with here are this kind of nice interface for this, and then number 2, like this actual, like, live chat functionality and maybe the ability to add articles. So we keep the user in context, That's what we're gonna be tackling today. I I think it's doable, not a 100% sure. Let's go in and start the clock on this. We will start our timer, 60 minutes and away we go. Alright. So as far as what type of functionality we want out of this, you know, what are the the jobs to be done or what are the goals for this specific project. We want to be able to chat chat with visitors in real time and then, you know, maybe display rich content inside the messenger. This could take the form of articles, help center stuff, updates, etcetera, and I totally cannot spell. How are we gonna get there though? Right? Normally, if you've watched any of the past episodes, at this point, I'm like doing some data modeling. The starting point for today is a little different in that I've already got a project that we're going to lean on. It's called Agency OS and it already has a a fully fleshed out data model. You can explore the front end of this at agencyos.dev. But this actual project you can download and test out yourself. It lives in our Directus Labs organization at, Directus dash labs slash agency dash os on GitHub. Basically, there's some instructions, but the main features of this include a website, CRM project tracker type of thing, and then you have a client portal. So on the front end, it kinda looks like this, your standard agency website. You know, we've got all these different blocks. It uses the page builder inside Directus. We got blog posts and articles. You know, there's a a help center that should be displaying some articles that doesn't. And then you have, this client portal where clients can log in, keep track of their different projects, see files, see their invoices, pay for those specific invoices. Right? This is this is pretty neat functionality. And then, you know, you would basically manage your entire workflow from your deals all the way through, like proposals, projects, and tasks inside your direct assistance. Right? So I can see a list of all the tasks that are necessary to complete this specific project, who's the organization, who are our contacts for that organization, And I can manage this whole thing here. But in this case, I'm using this because we've already got this conversations model and this messages collection. And we have things like help center collections and help center articles already mapped out, which is is really nice, just to kind of piggyback off of what I've already built in the past. So if we dive into conversations, you know, those are gonna be I can have multiple conversations ongoing at once. Those would be like the different threads. Inside that we've got like a user that created it. Maybe a visitor ID in this case, if the visitor is not known. We've got some messages inside there. Inside the messages, each message has a bit of text, who is the user or the visitor ID that created it, and away we go. That's what it looks like. We're gonna dive into like some of the real time features of Directus as well. So what have I actually got set up for you here? In starting this project, I've got a Nuxt application that I often use as a starter here for this. You know, the Nuxt application just has a direct us client using the SDK already preconfigured with, authentication and rest. I've got some login and register pages, so we can kinda simulate that. But that's kind of it. The other like 2 minutes worth of setup that I have done, this Agency OS project, before the V2 release, I had a janky little chat widget that I was trying to use in this specific project. So I can go back in time here and find this little chat widget. Let's just copy this in. And, it looks like I've already got it copied in, and cleaned up. But just a little chat box that we could use. And, I could never quite get this to work before the big release, so, it it ended up getting scrapped. So let's revisit this. I've got our chat widget. Let's drop this thing into our index page. So if we open up this incognito window so I don't spoil any of the, so I'm not using that direct to session token basically. Alright, so I've got my page here, I'm just going to drop this chat widget in there. And now I've just got this shell for a chat widget. It opens, it closes, it kinda looks nice. That's really all it does, right? There's this motionable component that uses the view motion View Use Motion, composables. Where is it? View Use Motion at view use slash motion. Just to get like some smooth animations using, I think it uses pop motion the hood. And it's just, tailwind for styling, right? Our content is gonna go in here. But let's fulfill that first thing, right? We wanna be able to chat with visitors in real time. I don't even know why these are bold, that's gonna aggravate me. Alright, how do we chat with visitors in real time? Directus has powerful real time functionality built in. Now inside my Docker Compose file for this, I've already got real time enabled. So that is the WebSockets enabled part here. And when you're working with real time and direct us, there's 3 authentication modes. There's public, handshake, and strict. Public just means anybody can open a connection. Handshake means that you don't have to provide the authentication as the you could still connect but the first message has to be authentication. And then strict means you have to pass the authentication before you actually initiate a connection. Alright, so what we're wanting to do here basically, we're gonna have to extend our plugin that we have. So I'm gonna go into my Directus plugin that I've got. And again, it's just creating a simple SDK client to work with. I'm gonna go in here and let's actually create a second client. Create Directus Real Time Client. So I'm just gonna create a new Directus client and I'll just copy paste this previous client. Great. Except with, we're not gonna use any of this, we're gonna use the real time composable. Alright? And there is an auth mode property in this case and I'm just gonna pass public because we want anybody to be able to connect to this. So I'm gonna create that client and then the next thing that I'm gonna do is provide that client to you, the Nuxt application. Let's just call it directus sws for directus web socket. And we're gonna pass directus ws. Okay. So now we have created this client. Our chat widget still doesn't do anything. That's fine. What we're gonna do is open up chat widget and we're going to access that client. So we'll do directus ws, because Nuxt provide when you use Nuxt provide it adds a dollar sign to the front of it. And we're just going to do use Nuxt app composable. That will give us access to our web socket client. And then we're going to actually open a connection, right. And this application is using like server side rendering inside Nuxt. So I probably want to like wrap this up inside a composable as well like the like an on mounted hook. So we'll await the Directus WebSocket connect. And I I don't work with Directus real time a lot, so we may get some funky behavior here that needs to be async. Okay. So I'm just gonna open up the console and see if I open up all and I go to web socket, we can show a connection here. It says switching protocols. Okay, AWS local host. Alright, so looks like we've got the WebSocket connection opened. Alright, now let's just kind of dig into the documentation a little bit. Because I the next thing is gonna be, like, subscribing, but we probably need some message handlers. So we'll go into get started with real time on the documentation. I am doing this, just as you might. So you can see that we've got the real time client here. We've opened a connection to the client, or at least I think we have. And then now we've got like this client. We need to listen for changes. Right? So I might as well just plop this in our on mounted hook. And I'm just gonna replace client dot with dollar sign directus ws. And now let's see what we actually get out of this. Do we see any actual messages here? I don't know why am I not seeing any messages. Directus cannot connect when state is open. Do we need to do we actually need to pass the auth mode to this? Is that an actual property? Switching protocols, directus.ws.subscribe. Let's subscribe to messages and see what happens. On message data dot undefined. Message. What if we just console log the message here? Console log message. Alright. Alright. So we see we're not getting anything there. Subscribe error, invalid collection. Okay. So not getting the correct collection. Let's just take a look at this. This is messages. Oh, let's try subscribing to messages, right? On open console log event open. I don't know why it is not not sure why it's not working correctly. You do not have permission to access this. Right? So as a public user, we don't have access to the individual messages. Alright, so two ways around this. We could potentially provide some authentication, if I can actually talk, right, or we could give access to this. Alright. So if I go into my access control settings, I'm gonna go to public in this case, and I'm not gonna give all access, but maybe we use custom. Right? 2 things here, we get the public can create messages, that's fine. But when it comes to reading messages, we don't want people to be able to read other people's conversations. So in this case, let's add just a rule to it. If the conversation, conversation visitor ID contains what? Can we even do that, right? We'd have to pass the let's do this instead. We'll use our website API user for this. And okay. We'll do messages. You can see conversations. I tell you what, let's just open the floodgates here just to test this out and make sure we can see these things. Alright, so we've got a subscription. There's our data, okay. And if I open this up and I add a new message, right, which is the test message, save this, we should see that data come through, which we do over here. Right? So we see our subscription for the messages. Do we see our actual data here? Great. There's our text, test message. Here's the user that created that. That's gonna be me, the admin user. And we've got the date created. Right? We've got all the details of this specific message. Alright. So at least now we are getting the messages. Alright, if we go back to the documentation, we're getting some messages here. That's great. You know, a couple things when we create a subscription, right? We can choose what event to subscribe to. So the event wise, we'll probably do like create. And we can even add a query, right. So imagine we've got a website visitor. We want to basically, like, create a unique session or a unique ID for that visitor, so that that user can only see their messages. Right? So what do we need to do there? This might be something like a Nuxt Middleware. Do I have do I have UUIDs in this? Okay. So I've just got like a function in here and and this is, like a lot of this is actually ripped from the AgisUS project to begin with. But I've just got a function here for generating an ID, which basically just creates like a UUIDv4. Alright, so what I'm gonna do here, let's just create like a session dot global dot ts. I'm gonna add this in the middleware folder inside Nuxt. And when I do this, if I use dot global that will run on every route. So I'm just gonna go here, I'm gonna copy this, and I don't really need to here. Basically, anytime we navigate to a route, we wanna do some things. Right. We want to, let's check like a session ID or visitor ID. Visitor ID equals use cookie so we can set a cookie that way we can access it server and client side. We got visitor ID. If visitor ID dot value, if there's no visitor ID dot value, we are going to set up a where is it? I'm gonna do Visitor.id.value equals generate ID. There. Okay. Alright so now basically we just created a middleware that's gonna run globally on every route within this application and it will create a cookie for the visitor ID. Okay. And now I can see that here that I've got a visitor ID cookie which is some random UUID value. If I delete that, refresh, we can see we get a new visitor ID. But that cookie will persist across sessions, which is nice, and we don't have to do anything else there. Okay, so as far as our query, if we just go back to the documentation, right, we can add a query, and we can do all of the same query params that we would in the regular Directus API. So we do something like filter where, well actually let's back up a minute, we need to get that cookie, right? So visitor ID is equal to use cookie visitor ID. Okay. And then for our filter, we want the messages. Let's think about this, right? We're gonna have to create a conversation. Let's just get some messages first, right? So we're gonna add the visitor ID to each message. Alright. So maybe we just wanna be able to see we're gonna have to like create a conversation. There's no way getting around that. Alright. How can we create items, right? So we can use that same connection and then we can actually get all of our like CRUD operations. So let's just do this. We're gonna actually set this up. I wanna subscribe to 2 items, right? Conversations. And in this case, we want to filter where the visitor ID is visitor.id.value. So inside our data model, right, when we create a conversation there's a visitor ID for it so we could track that. Great. Okay. So I could see that there. Then we have the individual messages, and we we've honestly got a lot of extra stuff that we're probably not even really gonna need here. But we got visitor. Id_id, that's what it is inside our database. And then we've got the cookie value here. So visitor dot ID, and then the param is gonna be like this, equals to visor.id.value. And then I think we can also pass a UID to this subscription. Let's look at it. Learn more about authentication, learn more about subscriptions with WebSockets. Alright. Using UID's type is subscribe multiple subscription UID. Okay. So we'll call this UID And this will be visitor conversations. Thank you, GitHub Copilot. Let's also open a connection for messages as well. Subscribe messages where messages dot create, query, UID. And in this case we can't just filter out for the visitor messages, right? We also have to be able to show the admin messages as well. So in this case what we're gonna do, we're gonna work on the conversations because we can use the related fields inside direct us collection. So I can go up a level here. It's actually conversation. Is what's the parameter gonna be? Right? So we could have potentially multiple conversations. Right? So we'll have conversations equals ref, and then we'll have messages. Actually, we might just store all of those. Right? Conversation, let's look for our query params, global query parameters for our actual filter here, filter rules, we're looking for items within an array. So we're going to use n, in conversations dot value map conversation dot ID. Okay. I think this should give us what we want. And if I open this now we can see that we've got a subscription for conversations, we've got a subscription for messages. I'm gonna just copy the visitor ID here. And the other thing I'm gonna do, I'm gonna create a new conversation. So we'll say new conversation. I'm gonna add that visitor ID here. We'll call this test convo. And do we get a message for that? We can see this conversation here. And now let's just test this as well. Will we get, TestConvo not from visitor? Just want to make sure. Okay. So you can see I created a new conversation that didn't have that user ID or that visitor ID. And now we don't actually see that. So how are we doing on time? We got about 35 minutes left. We're looking okay. So let's start having some conversations. Right? On messages, on init, Subscription init. Get conversations. Alright, subscribe on open on message. Okay, so basically all we're doing at this point is listening for messages. And just logging that out, right? So we need to do some other things here. Is there like a is there another property on a knit? No, a knit. I need more practice with real time, obviously. Alright. So when we receive the message, let's look and see what our references are here. Full code sample, I think there's actually a guide that I built on multi user chat or somebody built this. Alright. Connect cleanup, Subscribe. Okay. So you can get the actual subscription equals await client dot subscribe. Constant subscription equals ws await client.subscribe. And then we can get our initial messages. So here, let's just call this get initial combos. And we might also want to grab all the fields within that as well. So we're gonna get all the root level fields for the conversation, and then we're also gonna get all the message values included in that. And then let's populate conversations.value=subscription.data. I'm not even sure it's gonna be that. We'll just do it's actually console log subscription. See what we get there. Alright, subscription L2 generator. Cons to subscription. Subscription, unsubscribe, generator state suspended. For message of subscription, for await combo of subscription push combo. Subscription is not iterable. Let's just wrap it like in the documentation here and see what we actually get. Right? So now if I show let's just show the actual conversations. So we'll go into our content section, and we'll do divv4conversationinconversation. Flex call. Let's just add let's just log that out in conversation. Prepping the pretag. Type subscription event, init. Yeah. So this might need to actually be updated. Right? Subscription event, await client subscribe. Subscription Subscribe to messages, create event. What am I missing here? Subscribe if type. This message is create, receive message. Subscription started. Okay. We're not actually getting the message history there. Display historical messages. Okay. Alright. So basically to get all of the historical messages, we need to read the messages. So we'll do client dot, it's actually gonna be directus dot send message, and then we'll get all of the items within the conversations. So that'll just be items. The collection will be conversations. And the action is gonna be read here. So we just wanna read all of the conversations. And then as far as the query, we'll set a filter where visitor ID equals visitor ID and we'll just get all of those conversations. And maybe we limit those to like the past 10 conversations, kinda like our documentation here. Alright. Send message. Okay. So we're gonna get that message. Send message. There's our items that we could see. There's those conversations there. We could see the individual messages within those, but we're not actually populating that data somewhere. Right? So we're going to need on our message function here, basically if the data type equals items, okay, so when we receive a message, we are going to take our message. Alright. If the message dot type conversations there we go. GitHub Copilot knows what we want. And we'll just refresh when we open the connection. If I go in and we look for chat widget inside the dev tools, do we see our conversations? We don't. Alright. Why not? Message. Message, message. If let's just update this as well. Is connected. State is not open. Okay. So we're getting our messages. They're just not populating to what we want. Right? So we see the data is an array. That should be what we want though. If we open this up, we should be seeing those conversations. Direct to send messages, items, conversations, if equals conversations, conversations dot value equals messages dot data. Conversations. Yeah. That should be right. Collection equals conversations message dot type. What is the the type equals items? Oh, we don't really have a, maybe we wanna add a UID. Type equal to UID equals get combos. Alright. So now if we take a look at that, right, we should have that UID get combos. And we could just target it that way. If message dotuid equals get combos, and now we populate that data. Okay. Alright. So now we can see a conversation there. That was a lot of stress just to get this conversation. And let's just go in, and I'm gonna add, a message to this. Hi. Hey. Okay. Now if I refresh, we can see the actual messages within that. That's solid. Okay. And then we wanna do something like this where we have selected convo equals ref. Let's just call it conversation. Okay. Solid. Alright. So now we need to actually like start showing our our conversations. Right? So within this, we want to maybe wrap this up. Right? We'll do it in like a template tag. If there is no if no selected conversation, We're gonna show this information. Then we'll come down and do another template v if selected conversation will show something else. Alright. So instead of our actual conversations here, what if we show we wanna add a button. Do we want to make it a button? Yeah, we do. And then we'll do like the conversation p conversation dot title, I think we've got a title for that. Alright, so we refresh, we see test convo. Let's give each one of these a lot of padding. Okay. So we'll do like py6px4. You know, maybe we wrap these again. Div. Do like a divide y situation. Okay. Test convo. Test messages dot text. And, you know, maybe we show like the user avatar as well. Alright, so again, like we might have to drill down into this a little further and do messages dot user created, user underscore created. And this should give us like all the actual user data in addition to that. Right? So I wanna make sure that we've got let's see if we can get, like, the user avatar source equals, use files, conversations dot messages dot user ID. Can we show the user avatar? Where does this use files to? File ID. Alright, let's just check the messages real quick. Chat widget, conversations, messages. Are we getting user created? We're not getting that information, so we probably need to control that with access control as well. Alright. Users. Again, we're just gonna open the floodgates. Would not do this on a regular app. Make sure what other rules do we have here? Public. Okay. And I really don't even have a I don't think the admin user has an avatar here either. So let's just add one for that. All files, just added avatar for me. Refresh. Are we actually getting that avatar? We're not. Anonymous component chat widgets. We'll go in. Here's our conversations. Here's our messages. We'll get the user created. We could see the avatar, access control public, Directus files. Let's just give all access. Yep. Still not showing it. Use files. Let's just call it, like, constant file. It may not even be able to use files. What is actually this? Get probably need to look at the actual file URL. That's the actual method here. That's why I'm not getting that correct. Alright. So file URL. Okay. There we go. Alright. So that's not necessarily pretty or amazing. Let's flex it, test convo. Maybe we'll just show like the number of messages dot length. And wrap this. And then we do, p, maybe like get I got a getRelativeTime conversation. Oops. Just got to wrap that. Get relative time, conversation, date created. 16 minutes ago, we'll add a little bit of gap here. And let's do text left. Okay. Alright, so we got the test convo and now when I click the button at click, we want to set the selected conversation to the conversation dot ID. Okay. And now we can see that goes away. And now we just need to show the messages within that, right? So selected, let's do a conversation messages. Yep, get up Copilot knows what I want to do. If that is has no value, we're gonna return conversation messages. Let's see. Select a conversation dot value, conversations dot ID. Okay. Return conversation messages. Okay. Yeah. Sometimes AI is not to be trusted. Sometimes it's okay. Alright, so then let's just show the actual messages, right? Do the same thing. Let's copy this down. Div. Alright. And then we're gonna show we'll wrap this. And then we need a div for each message. So v for message in selected conversations, key equals message ID. Let's see what this shows us. Right? We've got the test conversation, selected conversation messages. Alright, so I select that. We can see this. We don't have the message text. Hey, there's the message text. We're going to need like some padding here. Okay. Maybe we wanna wrap each message in like a let's add just a little bit of padding. We'll wrap each message in white light gray, gray 100. And give it a little bit of padding. P p 2, Rounded XL. Okay. So there's our messages. How are we doing on time? We got like 17 minutes. We are quickly running out of time, right? So now the bottom of this, we can see the messages generated from the user here. And the other thing that I wanna do here is when we send a new message, or when we receive a message, right, if this is the, so we whenever we create new messages, it needs to be we're subscribing to the messages. Alright. So here, we're gonna need to pass the messages are received. We wanna add those add them to the proper conversation. Alright. So what is GitHub Copilot coming up with? We got conversations dot value dot find, message dot data dot conversation. Okay. If conversation dot push. All right, let's just test this out and see if we're getting what we actually want here. Local host 8055. Alright. So we got our convo there. I could click into it and see our test message. And now, like hopefully if I go in and add another message here and just save, We should get that, but we're not seeing that message. Test combo, yo. Why are we not receiving that? Console dot log dot message, web socket messages. Conversation In That should be a message of the conversations. Subscribe. Is that after maybe we need to wait getting those initial messages? All right, so let's open this up. Now we can see both of those messages, right? I do tttt, we're still not getting that message. So let's just drop this for now. We would come back and clean this up, but I I just wanna actually get the messages. Alright. T t t. Test. So we're getting that visitor messages array. It's just not populating to the proper conversation. Alright. So don't trust GitHub. If git combo and messages what's the actual message that we received? Git convo subscription event dot create visitor messages. It's basically we need to use that UID again. UID, those visitor messages and message data. I think that should take care of it. Test convo. Still not getting that. Right? Chat widget. Select a conversation messages, select a conversation, conversation. It's just not populating those messages here. What are we doing wrong? What are we doing wrong? If conversation oh, conversation dot value? Conversations dot value dot fun. That's not really gonna work, is it? So we need to like we're getting the conversation value. So here's the message. Here's the data. We're getting back an array of data actually. Conversations dot value dot find data. Message dot data is an array. Okay. Conversation. I don't even know if this is actually gonna work, is it? That shouldn't work. We'll test it and see. Yeah. It's not gonna work. Promise dot avatar. So we need to definitely add that as well. To our query, we're gonna add the same thing or similar, where we have fields, we're gonna have user created dot dollar sign. Okay. So we got our test convo. Can we receive messages from Reading an avatar. Man. Okay. Conversation dot messages dot push. Why is it not seeing the avatar? Chat widget, conversations, messages within that. It's pushing that into the array, message.data.0. That would be why. Alright. One more time again. I'm just gonna delete some of these. Save it. We refresh over here. We can see there's our convo. We do yo. We save. Okay. So now over here on the client, I can see that conversation or that message being populated. So we'll just add a little gap between these space space y 2. Okay. And we'll probably need to do like message dot user created dot if we have a message user dot created and message dot user created dot avatar. Alright, so we're only gonna show that if we have it. And then the next thing that we need to do in a hurry here, about 9 minutes, is to actually start populating these messages, right? So we want to add some form fields to this down here at the bottom somewhere. So if we're inside the selected conversation, great, let's add the message form. Cool. Alright, and we'll do, I've got this nice Nuxt UI library here. So we'll do Submit and Message and it just gives us some nice inputs here. Flex column, there's the wrapper. Where is this actually gonna be? Inset 0, uform, send message, v model, new message. Alright, so let's add our new message. New message equals ref, that'll be blank. Let's have a function for send message. Okay, send messages, create conversation, select a conversation dot value, new message dot value, user created. Nope, Where did you go? We can rely on AI here. And we don't want a UUID or UID for this. Oh, maybe we do. Send message. And we're we're not gonna use the user created. Alright. Does this give us, where's my little bar at? User, User selected conversation. There's no selected conversations. Vfusermessage.created. Oh, message dot user created dot avatar. Okay. So now we're going to form. The form should actually be pushed to the bottom. So that'll be flex 1 height full. Okay. Then inside this form, I want that to flex, justify between. Okay. And we add a bit of padding to that as well, p 2. Give it a border, border t. And we stick it to the bottom. Nope. This will be like overflow. Why? Auto. Okay. Flex justify between, and this could probably even be like stuck to the bottom in the absolute. Alright. So let's just change this to like text area. Hey hey hey. And now we can see we can actually chat with that specific person, inside our send message, we're probably gonna delete that new message as well. Just set that value once that's actually populating. Okay. So now I can see my convos. I can open up that convo. I can send a message to their team. Great. How could we actually show the help center articles? Right? In 5 minutes, can we get this rich content within there? We're going to be pushing that, right? Pushing it quite a bit. Alright, so we got our chat widget. We're probably gonna need like a page functionality or something. Like hey, what page are we on? The default is, let's set that to chat. So this will be this is our conversations. And can we wrap this in another template tag? Or it might be easier just to stick it inside a div. Alright, so div if page equals chat, We're gonna show that. Alright, so we should still see that now when we open this up. Still see that. Great. But let's just show another page. Div v if page equals articles. Alright. So then we're gonna show the articles. Maybe we show like a card v 4, Article in articles. I don't think any of these are actually gonna apply. View card, we'll just do like a p tag for the article dot title And see what this shows. Key is equal to article dot ID. Alright. And now we need like some kind of widget at the bottom of this to actually show, right? We need like a menu. So let's do div, do you like buttons? This is not at all. Show an icon. Name equals herocon's chat bubble left ellipsis. Let's just call this chat. And actually, and then we need a new button for icon name equals articles. How are we doing on time? 3 minutes. Are we gonna actually get this or not? Let's see what this shows. Chat. This is gonna be absolute bottom 0, p 4flexgap4, give that a little gap. I don't particularly like the icons for this. Let's just okay. And then if click at click, this is gonna be page equals articles. There we go. And at click page is equal to chat. Okay. Is that actually gonna work? Page is equal to chat. Page is equal to articles. Okay. So now we're switching those back and forth. We just don't have any articles, right? So let's use our client here. We've got articles. We could do a ref. Just make that an array. And then we'll have like a function, get articles. And we can use that same connection again. Right? Fetch articles. Yeah. I could use the regular rest connection as well in this case. But let's try to use this. Send message. We want collection of articles. This is gonna be a read. And the data, UID, we'll just do get articles. Okay. Oh, how we doing? We got 1 minute 20 seconds. Get articles. That's our function. Okay. And then on our receive bit here, if message dot UID and message data articles dot value dot messages. Okay. What else do we what else do we need to do here? We need to fetch those articles first, right? So let's do like a watcher. Watch page. New page. If page equals articles, get articles. Is this actually gonna work? Error. Invalid collection. Oh, no. We don't have articles. They're what? Post. Post. Post. So what is this? There we go. With what time to spare? 21 seconds. Right? Okay. So we can actually see a list of the articles. That's about as good as we're going to get in in this amount of time left. I'm calling that a win. So we can actually show the list of articles here. The next part would be to just probably actually, like, set these up to to, like, be able to read the rich text for those articles right within the little widget. But, you know, that is a wrap. Great. And if I open up Directus, we can see all the messages within that conversation. And honestly, I would probably build some kind of interface over here inside Directus to be able to manage this. Alright. So if we just wrap this one up, did we actually get to the intercom messenger? You know what? In an hour, almost impossible to rebuild. I'm I'm calling that now unless you built it before. But, yeah, far cry. But hey, the basics are there. And it goes to show you just how, how far along you can get with Directus in an hour using the tools that are provided. So that's it for this episode of 100 Apps, 100 Hours. Hope you enjoyed following along. You could probably see the sweat bubble up on my face at this point, but stay tuned for the next one. We'll see you.","published",[135],{"people_id":136},{"id":137,"first_name":138,"last_name":139,"avatar":140,"bio":141,"links":142},"791e1503-1d88-463d-9347-0b9192933576","Bryant","Gillespie","9013afc8-e8d7-4182-9b18-44db08117bb9","Developer Advocate at Directus",[143,146],{"url":144,"service":145},"https://directus.io/team/bryant-gillespie","website",{"service":147,"url":148},"github","https://github.com/bryantgillespie",[],{"id":151,"number":152,"year":153,"episodes":154,"show":164},"14fda5f2-95de-4dbe-a4e2-3fd956c21c19",2,"2024",[155,156,157,158,159,160,161,122,162,163],"9a3a8ffa-a27b-421c-93cf-3da2dcb726e9","d072a935-906e-4208-a5dc-e9b117d0ab29","b9f1d4cf-f53c-49db-9e87-adf7e3b9ff99","aad8d674-2b58-4604-8e43-b98f7c6e05cb","6bff0c09-ad87-4d5c-b227-89b8c3c02220","6fb9aa9a-2b59-44b6-b78f-d1831fa657c6","620cf225-a23a-415a-ad95-9ba8e2dec984","385bdd7d-038d-4f9c-8037-357e5272420a","383c24d5-b6b5-4d66-aba6-6997af5f77b4",{"title":165,"tile":166},"100 Apps In 100 Hours","fb0f9d45-be21-4634-94d4-2ef1cc5146f2",{"title":8,"meta_description":8},{"id":169,"slug":170,"season":171,"vimeo_id":172,"description":173,"tile":174,"length":175,"resources":8,"people":8,"episode_number":176,"published":177,"title":178,"video_transcript_html":179,"video_transcript_text":180,"content":8,"seo":181,"status":133,"episode_people":182,"recommendations":184},"ec88bef1-fffd-43eb-9d93-3123dc381b97","ai-letters-to-santa","d6b229fe-38fc-495b-ba0c-c574ebfea38f","1059428648","Bryant builds a holiday-themed app that generates personalized letters from \"Open Source Santa\" based on GitHub profiles. Watch as he creates a system that analyzes developers' repositories, determines whether they're on the open source naughty or nice list, and generates snarky, sarcastic letters from Santa — complete with festive styling and holiday cheer.","6209314e-e6ee-4a2d-9e97-11eedd08595a",59,1,"2025-03-10","Mission: AI Letters to Santa","\u003Cp>Speaker 0: Alright. Alright. Alright. We are back with the Christmas edition of 100 apps, one hundred hours. Today, we are going to be building AI letters from Santa.\u003C/p>\u003Cp>I've got my lumberjack style on today. My wife called this lumberjack Jesus earlier, but I digress. We're back for more. The rules of 100 apps, one hundred hours. If this is your first show, we have sixty minutes to plan and build an application, a website, a portal, whatever.\u003C/p>\u003Cp>Whatever we're building, sixty minutes, no more, no less. And rule number two, the anti rule, use whatever you have at your disposal. And since this is an AI Christmas special, I'm gonna pull out all the stops. So let's dive right in. We're gonna hit the clock here.\u003C/p>\u003Cp>Fire it up. Sixty minutes on the timer. Go. Alright. So AI letters from Santa.\u003C/p>\u003Cp>What do we actually want out of this? So I have to admit, I cheated a little bit because I thought about this with my team, and I knew we wanted to do this. I've seen things in the past where you write a letter to Santa, you get something back in the mail, etcetera, etcetera. With AI, we could take this up a notch. So combining two ideas.\u003C/p>\u003Cp>A while back, I saw a GitHub roast page where you enter in your GitHub profile and it, basically will scrape that and give you a roast of how well you're actually not doing in GitHub. So we're gonna combine that with a letter from Santa. And basically, what we wanna do is, enter a GitHub profile. We're gonna scrape that profile. We're going to send that to AI.\u003C/p>\u003Cp>So we want LLM analysis of the profile. I'm not sure what we're gonna call that. And then I'm going to bucket people on the open source naughty or nice list. So score naughty or nice list. And then we're gonna generate a letter from open source Santa.\u003C/p>\u003Cp>Generate a letter from open source Santa to that GitHub profile to that profile. Alright. So as far as that functionality, this looks pretty good. Right? What are the tools that we're gonna use of the trade today?\u003C/p>\u003Cp>I've got a Directus Docker container up and running locally. Directus is obviously the back end we're using to store all of these things. And if everything works as intended p m p m g. I guess, sometimes things don't work as you intend. I've got a Nuxt application that we are going to try and use here.\u003C/p>\u003Cp>I'm not sure what's going on, but let's hop into the Directus instance. So I'm just gonna pull up Chrome. We'll log in to 8055, and I should be able to pull up my back end. So great. Got Directus running.\u003C/p>\u003Cp>You could see this is a pretty blank instance of Directus. This is just the boilerplate I use now. There are a couple extensions installed that I was testing, just messing around with. But, let's make sure. What are we doing here?\u003C/p>\u003Cp>For MPMI. Sometimes these things never go as planned. Okay. So maybe now we can get this Nuxt application up and running that will be served at local host 3,000, and we'll just basically use it to scaffold out our communications. As far as what I'm using, I've upgraded this boilerplate that I've used for 100 apps to, the Nuxt UI v three alpha, just to play around with Tailwind four and, you know, some of these nice new components that are coming from, like, Radix view.\u003C/p>\u003Cp>So alright. Let's actually model this thing out. Right? What do we need as far as our data models? I think we just need, like, a maybe like a profiles.\u003C/p>\u003Cp>So under profiles, we would have, what, our username, letter from Santa, letter from Santa, list, you know, are you naughty or nice? Great. And what else do we need? Let's let's jam on that. So we'll, just set up the back end for this.\u003C/p>\u003Cp>I'm just gonna create a new collection. We're going to call this profiles is the name of it. And why can't I zoom way in? There we go. That's maybe too far, but all good.\u003C/p>\u003Cp>Let's do created at, updated at. K. Status sort not needed. Who this was created by, I'm not super concerned with. So now we have a profile.\u003C/p>\u003Cp>We're gonna do the username. Great. That's where we'll store the GitHub profile. What else do we need? What else did we have here?\u003C/p>\u003Cp>We've got the letter from Santa. What is that gonna be in letter from let's just call it letter. Great. We'll use the WYSIWYG editor inside Directus so we could just store, I'm assuming, HTML content for that. And then we've got the list.\u003C/p>\u003Cp>So that's basically gonna be a string. We can, you know, make this look nice inside the directus admin. We'll just give it a naughty. Feel naughty just typing that out, and then we have the nice list. Great.\u003C/p>\u003Cp>There we go. And I'm just going to go on record that we're probably as soon as we start typing naughty into the AI stuff, we'll probably get some some things back. Like a I probably set off the content alarms or something like that. So there we go. We've got a username.\u003C/p>\u003Cp>We've got a letter. We've got a list. You know, I could potentially put that in here. What I'm gonna do now, I'm just gonna let's just we're just using Directus to store this at the moment. Right?\u003C/p>\u003Cp>There's a lot of different ways I could go with how to actually generate the application here. But Directus allows me to create custom extensions. What I'm gonna do here is just start, working on this from the Nuxt side of it. So we're gonna input the, actual form here. Let's add a profile.\u003C/p>\u003Cp>What are we gonna call this? Let's just call this letters dot view. We'll get a view component set up. Lang equals TS. Great.\u003C/p>\u003Cp>I need to work on my little macros here. Okay. Alright. The other thing that you'll notice here is that I am using cursor. So cursor I recently started testing this thing out.\u003C/p>\u003Cp>Really enjoying the actual auto completions for this thing. So, I don't have it usually generate like a a giant list of code, but the automations, or the auto completions are are pretty nice for this thing. So let's start with, what, step one. It'd be enter GitHub profile info. GitHub username.\u003C/p>\u003Cp>Alright. So the only thing here, sometimes it gets a little wonky with the okay. So we use the you form from Nuxt UI. Good question, Brian. The new one, the alpha, they changed some of the conventions.\u003C/p>\u003Cp>So I've got a form with a schema. I've got a form field instead of a form group, and then I've got an input. Okay. So we've got the form, new form field, and input GitHub username. Let's just see what that gets us on the front end.\u003C/p>\u003Cp>We're gonna go to this page, which is letters. Okay. Alright. So let's go ahead and just center this up. I think there's actually a container component we can use.\u003C/p>\u003Cp>Great. Cool. Okay. So now we have a GitHub username, and let's add a submit button. New button, click handle submit, and boom.\u003C/p>\u003Cp>We have a GitHub username, blah blah blah. Hit submit. Supposedly does something. What it's gonna do right now? Absolutely nothing.\u003C/p>\u003Cp>Alright. So the next thing that we wanna do, let's kick this thing off. We want to have the form state, we use reactive for that. Great. GitHub username.\u003C/p>\u003Cp>Okay. And then we're gonna write a function to handle submit. Thank you. Yeah. Great.\u003C/p>\u003Cp>We'll just, console log that. Right? Boom. There we go. We can see the GitHub username, yada yada yada.\u003C/p>\u003Cp>Alright. This is actually gonna be an async function. Great. Okay. So now what do we wanna do with this?\u003C/p>\u003Cp>Right? We have to think about our application structure. And what I'm gonna do here is just basically add a Nuxt server route. So if we break this down, in this server route, what we're gonna do, call the GitHub API, call GitHub API. We're gonna wanna grab a couple pieces of information like the user profile, or any other repos, and maybe, like, their their public read me.\u003C/p>\u003Cp>I guess we could loop through the actual repos and, you know, pick up more information there, but, let's see what we can get done with that piece. Alright. So let's just go here. We're gonna set up a new route. Let's call it, roast route.\u003C/p>\u003Cp>We'll do post, and just gonna copy the event handler here. K. So now whenever we hit this route with a post, it should return hello world. We can just check and see if that's actually gonna work. So, we will do the I'm trying to think if that's gonna return.\u003C/p>\u003Cp>Nope. So we got the response. We're gonna do await. We can use the regular fetch or the dollar sign fetch, which is the Nuxt specific version. And just test this out, see what we get back in the console.\u003C/p>\u003Cp>Where are you? Okay. Yeah. So we can see the request going out. We can see hello world coming back.\u003C/p>\u003Cp>Great. Cool. Alright. So now what we're gonna do, right, let's just scaffold this out. We are going to pick up the body.\u003C/p>\u003Cp>There's a wait read body. Great. So that is going to have the GitHub username in there. And, how did we spell that? Yep.\u003C/p>\u003Cp>Great. Alright. So we're gonna say GitHub username, and then we've got, like, this git roast function. I'm not really sure where some of these auto completions are coming from. But, what we're gonna do next, let's call yep.\u003C/p>\u003Cp>There we go. That's a good one. API users, GitHub username. Is that the correct one? Let's just test that.\u003C/p>\u003Cp>All the developers on my team are screaming and crying at the moment, watching all these AI auto completions. So that seems fair. Great. And let's actually use the Nuxt equivalent. Just this is using OFETCH, which does some automated data transformation and should automatically throw errors for you as well, which is nice.\u003C/p>\u003Cp>So this is gonna be the let's call this a profile. Alright. And then if we take a look at the profile, we probably wanna get the actual repos for that user as well. Alright. So we'll get the repos.\u003C/p>\u003Cp>Great. And let's just take a look at the data we're getting from the actual repos. Okay. So what do we actually concern ourself with here? Do we actually want all of this information?\u003C/p>\u003Cp>What do we actually care about from these? So stargazers, watchers counts, maybe those properties. You know what? Let's just jam it all in there and see what comes out of it. Right?\u003C/p>\u003Cp>And then let's get the profile readme. GitHub user content, GitHub username, repos dot name, Repos dot name. No. That's not gonna cut it. I think it's gonna be, what, GitHub username.\u003C/p>\u003Cp>GitHub username. Somebody who's already done this before. Give me the structure. And then main. Let's just see if we can find that.\u003C/p>\u003Cp>Read me will just populate my name. I don't even know if I have a actually have a read me. Yeah. There we go. Okay.\u003C/p>\u003Cp>So that is the structure. Great. That is what we needed to confirm. And now let's just actually return this and see what we'll get back. Alright.\u003C/p>\u003Cp>Brian Gillespie. Now I'm gonna fire this away. Roast. No. Nothing found.\u003C/p>\u003Cp>Well, that's a little concerning. GitHub username equals body. Read the body. We have fetched the user's GitHub username. Let's just console log the username.\u003C/p>\u003Cp>API dot GitHub users. I don't see the actual username coming back. GitHub. That's always fun. Alright.\u003C/p>\u003Cp>What did I do wrong? GitHub underscore username. Okay. Oh, duh. Are we actually passing that in the body of the form?\u003C/p>\u003Cp>Form. Console dot log response. Request payload. API slash roast dot post. What are we getting back here?\u003C/p>\u003Cp>GitHub username form dot GitHub username. Oh, that's right. We are missing a state variable here. So is it actually submitting the form? No?\u003C/p>\u003Cp>No. Okay, friends. What do we do from here? We have handle submit. We're going to use fetch await.\u003C/p>\u003Cp>Wait that fetch request. We should have already got this back. Of course. There it is. What a dumb dumb.\u003C/p>\u003Cp>Forgot to actually fix the v model there. So that's what that is. Sometimes, these are not great to do at the end of the day. But okay. Where we at as far as time?\u003C/p>\u003Cp>We've got forty two minutes remaining. I feel pretty confident on this one. Alright. Now with our roast, we can remove this. We should be able to get that information.\u003C/p>\u003Cp>Now let's make sure that we're getting what we want back from that API. Great. There's the profile. There's the repos, and there's the profile. Read me.\u003C/p>\u003Cp>Great. Alright. So what are we gonna do with these now? Right? The next step in this process would be to, pass the profile to LLM and ask it to summarize for us.\u003C/p>\u003Cp>What do we want this to return? It should return something that looks like this. We want a letter from Santa. Letter from Santa in HTML. And then we're gonna want the the list, naughty or nice.\u003C/p>\u003Cp>And that should be all we really need to return. Alright. LLM returns JSON. Cool. So what is the LLM we're gonna use?\u003C/p>\u003Cp>You know, typically, I use OpenAI for a lot of the stuff that I do here at Directus. I've been messing around a lot with, Claude locally. So we're just going to try this out. Santa letters. So we're gonna use anthropic.\u003C/p>\u003Cp>There's my API key. We're going to drop that in our ENV file, if I can actually get there. Jeez. There we go. I'm just gonna call it Claude ABI key.\u003C/p>\u003Cp>Okay. Great. And by the time you've watched this, hopefully, I've disabled that key. So, don't stop the video and try to figure that out. Alright.\u003C/p>\u003Cp>Let's pull up our docs for the API. We need to get the API reference. And let's define this prompt. Prompt. You are a letter writing AI.\u003C/p>\u003Cp>Alright. Analyze the following GitHub profile. You are the open source Santa Claus. You determine whose open source contributions are naughty or nice, analyze the following GitHub profile, Return a JSON object with the following fields, a letter from Santa and HTML. Set a really high bar for the nice list.\u003C/p>\u003Cp>What else do we need as far as a prompt? And, yeah, here is the data, profile, readme, json, stringify. Wonder why it's doing that. But okay. Nevertheless, there we go.\u003C/p>\u003Cp>Turn a JSON object instead of really write the letter in a snarky sarcastic tone. Cool. Alright. And now we're going to send that to Anthropic. Alright.\u003C/p>\u003Cp>So if we look at their oh, looks like we could just use their JavaScript SDK. That's great. Let's go ahead and open this up. We'll fire that up, install this thing. Import anthropic.\u003C/p>\u003Cp>Great. And then we're going to create that message. Alright. Constant AI response equals anthropic messages dot create Claude Sonnet. Okay.\u003C/p>\u003Cp>Messages user role, content prompt. Do we wanna set, like, max tokens? What is the what's the default for max tokens? Where do we actually pass this API key? Getting started authentication, x API key.\u003C/p>\u003Cp>API key equals process e n v. And, again, like, you could start to see why I really like using cursor because it has, like, this sixth sense for a lot of this stuff that I'm actually trying to do. Sometimes it gets that wrong, but a lot of times it gets it right. So alright, AI response messages. Do we wanna set a max tokens?\u003C/p>\u003Cp>Body messages, max tokens required. Let's give some more parameters. Write a short letter in a short in a snarky sarcastic tone. That is 500 words or less. And then for the tokens, if we look at Sonnet, we've got like a context window of like 200,000, so maybe a hundred thousand tokens.\u003C/p>\u003Cp>Oh, no. That's the output. Max output is eight nine one two. That's fine. Max tokens.\u003C/p>\u003Cp>Great. And let's return. Actually, what we're gonna do next is save that to the Directus database. Right? So we've got this collection for our profile.\u003C/p>\u003Cp>What I've also done, I've got a utility set up here. This is just using the Directus SDK. And one of the nice things about Nuxt, I say that a lot, is, the ability to it will auto import this for me. So I don't have to import it. I should just be able to call Directus server right here.\u003C/p>\u003Cp>So let's call it Directus response equals await directus server dot request create item. That's going to be in the profile. And we'll do the GitHub username. That's actually going to be username. Letter response, content dot text.\u003C/p>\u003Cp>I don't actually know what we're gonna get back directly. Return only a JSON object. And maybe we wanna add something like this for let's just do code. We'll set this up. And I'm just gonna add a field for, let's call it metadata or something where I'm just gonna store the entire response.\u003C/p>\u003Cp>And honestly, let's just do that to begin with. Metadata, AI response, content dot text. So if we take a look at the API reference, we go back to messages here. I'm kinda curious as to what we're gonna get back. The content text.\u003C/p>\u003Cp>Okay. Type text something. We'll get back something from the system. Let's just even do it this way. We'll say content direct us response, and then we're going to return direct us response.\u003C/p>\u003Cp>See what that gives us. Now let's go in. Where's our app? We'll switch back to Chrome. I do like Arc.\u003C/p>\u003Cp>I've found it to be lacking for development because it's just not super fast. Alright. So fingers crossed that this actually does what it should do. And, let's make this even nicer. And we'll add, like, a loading state, constant loading, ref equals false.\u003C/p>\u003Cp>We'll add loading dot value equals true. Loading dot value equals false. Great. And what else do we want to do? Is there a loading state on the actual form?\u003C/p>\u003Cp>Let's take a look. So Nuxt UI state, there is not a loading state on that. There should be on the button though. So just update that. Okay.\u003C/p>\u003Cp>And let's test this bad way out. Submit. Alright. We're waiting. We're waiting.\u003C/p>\u003Cp>We're waiting. We're waiting. We're waiting. This could take a minute. So, you know, we might even want to, like, potentially set up a oh, okay.\u003C/p>\u003Cp>So we're not getting anything back. We see a request error. So let's go into our roast, and we should probably do some error handling. Alright. Catch error, console error.\u003C/p>\u003Cp>Return, or we could just throw the error. What do we got here? Format. Alright. Let's refresh.\u003C/p>\u003Cp>I'll try this again and see what kind of error we're getting and why. Pending. Invalid user credentials for Directus. Okay. Great.\u003C/p>\u003Cp>So, just wasting tokens there, throwing them into the void. One of the things that you'll notice, I do have this direct as URL set up, but, my server token is probably a % not correct. So I'm gonna go in and create a token for this. We'll just create a new token. We'll call this the server token.\u003C/p>\u003Cp>And I wanna make sure in my utility that I have that set as server token, direct us URL. Okay. Let's try this thing again. PPM dev. I will restart the dev server, pull in that new ENV, though I think Nuxt may automatically update that for us.\u003C/p>\u003Cp>How we doing on time? We got twenty nine minutes left, so I'm feeling pretty confident that we can get something out of this. Let's go ahead and try it again. Bryant Gillespie. Submit.\u003C/p>\u003Cp>K. K. Roast. You do not have permission to access this. Okay.\u003C/p>\u003Cp>Can anybody spot the error? It is because I left off a s. We have profiles, and this is profile. So again, if I I don't know if I you can actually see the logs for anthropic. Okay.\u003C/p>\u003Cp>Yeah. We could see here's the actual logs. It's probably not showing what we've got there. But anyway alright. We'll try this one more time.\u003C/p>\u003Cp>Let's just clean this up a bit. And away we go. Dun dun dun. I don't like the looks of this, actually. Let's just reset.\u003C/p>\u003Cp>Try this again. AI response. We got the prompt. Got the profile. Dun dun dun.\u003C/p>\u003Cp>The moment of truth. Are we actually gonna be able to get this thing to work? Brig Gillespie. Submit. Obviously, this would probably be better as, like, a background job or something like that.\u003C/p>\u003Cp>Alright. So we refresh, and we have something here. Okay. Yeah. So we're getting some text back.\u003C/p>\u003Cp>It looks like we need to parse the JSON. The letter is going to be text parsed response, text, parse response dot list, and then we get metadata, which would just be the parse response, I'm assuming. Alright. We're gonna delete this out. Let's run this again.\u003C/p>\u003Cp>And hopefully I'm not burning through all these credits that I loaded up. Okay. So now we're looking great. Okay. So we have our username.\u003C/p>\u003Cp>We've got our letter. Ho ho ho. What do we have here? Another developer thinking they can impress Santa with a few measly repositories. I've seen l's with more impressive profiles.\u003C/p>\u003Cp>I got made it to the naughty list. Great. Amazing. Alright. So now that's working as intended.\u003C/p>\u003Cp>Let's let's make this pretty. Right? The form, we're going to do max width. Maybe we set this to Excel. Move that form to the somewhat in the center of the page.\u003C/p>\u003Cp>And let's just lean on AI here. Right? This is already pretty cool. One of the other things I wanna do is maybe we set up a route where we actually surface this letter. Right?\u003C/p>\u003Cp>So if we do let's do letters as a directory inside pages. And we're gonna do the username in brackets. So just take this username, make that in brackets, and then I'm gonna put letters inside here, and we'll change the name of this to the index route. Alright. So let's just clean this up a bit, wrap this up, and console the error loading.\u003C/p>\u003Cp>Actually, we could do that in finally. Great. And what we're gonna do, if the response is good, we could navigate to the username page. Cool. And that way, you know, basically, like, this could get very expensive if if you made this thing public.\u003C/p>\u003Cp>Right? You don't want people generating like 35 letters to Santa. So we can add a check to the database if we've already got that GitHub username and just return the letter that we we already have. Right? Okay.\u003C/p>\u003Cp>So on the response, as long as there's no error, we're going to navigate dot to form. Github username. And this would be await. Navigate to. Cool.\u003C/p>\u003Cp>Alright. Now let's just lean on AI and see what we could do. Add some Christmas theming to this. Let's see what this actually will do. Add some Christmas thinging, ho ho ho.\u003C/p>\u003Cp>Looks like it's generating some random messages. Code review letters to Santa, random message, decorative elements. Great. Love decorative elements. Now, with cursor, I'm just gonna click apply here.\u003C/p>\u003Cp>It should go through and run through this actual code. I can close this out and see, you know, in kind of a preview way what it's gonna change. And if we hit reload oh, what we got going on here? Letters index. Is that because I changed the route?\u003C/p>\u003Cp>Okay. Yeah. Now we're looking very festive here. This looks this looks great. AI, what can you do?\u003C/p>\u003Cp>Alright. The other thing I see, maybe we want this to be block. Will that get it done? Block. Class.\u003C/p>\u003Cp>Let's just make the width full. Width full. Okay. And then let's shrink this actual form a bit. Yeah.\u003C/p>\u003Cp>MD. There we go. Alright. We're deep in the Christmas cheer now. And, while we wait, let's well, not while we wait.\u003C/p>\u003Cp>Let's actually go in and now we're gonna work on this letter. Alright. So, this does have a Nux plug in. This is just my boilerplate where I can go in and actually request the information from Directus on the client side, or, you know, I could set up a route for this on the server side in Nuxt. Both of those ways are are totally valid depending on your application.\u003C/p>\u003Cp>Obviously, totally up to you. We will just, let's let's keep it the same theme. We're going to, like, fetch roast, or we could do, like, a roast.git.ts. And what are we gonna pass? Do we want to pass the username as a param, or we'll just pass it as a query parameter?\u003C/p>\u003Cp>Okay. So in this one, what we're gonna do, we will call the profiles endpoint inside Directus. So we'll just go const, response equals await Directus server. And, you know, sometimes you wanna make requests on the server side. That's why I've got this set up, this way.\u003C/p>\u003Cp>We're gonna do read item, and I gave this a UUID. We could've used the actual profile as the primary key. But, what we're gonna do, read profiles, and we're gonna set up a query for this. So we'll do a filter parameter, the username. So that's the field.\u003C/p>\u003Cp>We're gonna drop down again. This will be equal to the username. So first we're gonna have to get the username equals get router param. Nope. We're gonna do git query, and that would just be the query.\u003C/p>\u003Cp>Great. Username. We could destructure this if we wanted to. Return username equals username, and we're gonna return that response. Great.\u003C/p>\u003Cp>Cool. So now we do this. And on this one, what we can do is use the use fetch composable from Nuxt. So this will be we've got some data. We're gonna use fetch, and we're gonna call API slash roast.\u003C/p>\u003Cp>And the is it params? I believe. See what we got. And let's add the so the same festivities, I guess. Festiveness.\u003C/p>\u003Cp>Perfect. Alright. Decorative elements, blah blah blah, random messages. We're gonna put that up here in the script. Okay.\u003C/p>\u003Cp>Code review letters to Santa. And instead of the form, right, we're gonna replace this with data. Alright. So now if I do this, what's gonna happen? Route is not defined.\u003C/p>\u003Cp>Okay. So we just need to call use route to fetch that route. And do we actually get the stuff that we need here? We could test this API as well. Letters API roast username equals Brian Gillespie.\u003C/p>\u003Cp>Okay. Yeah. So that's getting us what we want from direct us except is it query? What is the use fetch? This is where, like, Nuxt documentation comes in handy.\u003C/p>\u003Cp>Use fetch. Where we at? We got sixteen minutes remaining. We got data use fetch. What are the URL query?\u003C/p>\u003Cp>Okay. Alias for query. That's what I thought. Root params username. Oh, data.\u003C/p>\u003Cp>Are we actually let's jump into the view dev tools. We'll hit the username route. And I see the data here. Here's the issue. Right?\u003C/p>\u003Cp>It is returning an array. So inside our routes, we could, you know, do something like this where we're just picking off the first item. I could also do that transform that on the the Nuxt side if I wanted to. Here's our letter from Santa. Cool.\u003C/p>\u003Cp>Code review letters from Santa. What I'm gonna do, let's use the pros class from Tailwind to get styling for this. We'll make the text dark green. Great. That's fine.\u003C/p>\u003Cp>And then the interior of this, we're just going to use v HTML. So we get this. Do I not have Tailwind typography into this? At plug in Tailwind typography. Okay.\u003C/p>\u003Cp>Yeah. So there we go. Now we've got the letter from Santa Claus. This is looking really nice. Perfect.\u003C/p>\u003Cp>Let's add like a cursive font. Right? Font family cursive. And this is Tailwind four, where all the config is basically CSS variables. So, really enjoying that Nuxt module, playing around with it.\u003C/p>\u003Cp>Let's find a handwritten font. Okay. Caveat. Looks nice. Nuxt has also added a a like this font amazing thing where you just throw your fonts in the CSS and it will actually download these things for you.\u003C/p>\u003Cp>So let's take a look at this. Right? I'm just gonna change this to font cursive and bada bing bada boom, we get what we want. So let's put, like, pros XL to XL. And there we go.\u003C/p>\u003Cp>So dear Bryant, what do we have here? Blah blah blah, etcetera. We have got thirteen minutes left on the clock. What can we do for fun? Let's go back and actually test this thing out.\u003C/p>\u003Cp>I'm just gonna refresh. There we go. I'm gonna do our fearless leader here at Directus, mister Ben Haines. We're gonna send this to Santa, and something bad happened. We could not find okay.\u003C/p>\u003Cp>So it looks like this one is not finding Ben's profile. Haynes, Maine. And Haynes Haynes Haynes Haynes Haynes. Would that be at, like, Master branch maybe? Where's our roast?\u003C/p>\u003Cp>Roast.post, profile read me. Try. I bet it's at master. I'm just gonna do this the quick and dirty way. Alright.\u003C/p>\u003Cp>So we go back. Let's try this again. Mister Ben Haines, we're about to roast you, sir. Alright. So we're checking the list twice.\u003C/p>\u003Cp>And eleven minutes on the clock. We've got the letter to Ben Haines from Ben Haines. Why are we not seeing the actual letter? There it is. I'm dreaming of a Nuxt application that actually works.\u003C/p>\u003Cp>What is going on with this? Letters, username, data dot name. I'm assuming because there is no name. Username. I'm running this on a sour note here.\u003C/p>\u003Cp>Ben Haines. That's kinda weird. Ben Haines. Why is it doing that? API roast username.\u003C/p>\u003Cp>What is going on here? Get async data. API roast. Why does it work for me and not for mister Haynes? What are we actually doing wrong here?\u003C/p>\u003Cp>Did I spell the name wrong? GitHub username. Alright. E pipe. Use fetch roast.\u003C/p>\u003Cp>Can't find the username? Profiles get username, get query. Is it read query? No. It's get query.\u003C/p>\u003Cp>Return query. API API roast. Ben Haines. So why aren't we why isn't this working? So it's not actually finding the username for that, which is odd because I have the username right there.\u003C/p>\u003Cp>Filter contains. Okay. I don't understand it, but we're gonna roll with it. Great. Some type of encoding or something maybe.\u003C/p>\u003Cp>Not sure. Booyah. Ben, I'm gonna read this to you. Dear Ben, ho ho ho. Well, isn't this embarrassing?\u003C/p>\u003Cp>I've been reviewing your GitHub profile, and I must say I'm thoroughly underwhelmed. 20 whole repositories, you must have been super busy this century. Meanwhile, Santa's got billions of believers worldwide. Look, I'm not saying you're on the naughty list because your contributions are lackluster. I'm saying if you were open source for coal, you barely have enough to heat a dollhouse.\u003C/p>\u003Cp>That is brutal. So let's call that a win. This is AI letters with Santa. Do we wanna do one more just for fun? Just for giggles?\u003C/p>\u003Cp>Let's let's test this out. Directus Directus. I forget Reich's actual, GitHub profile. There it is. Okay.\u003C/p>\u003Cp>So we're gonna throw mister Reich Van Zanten in there, our CTO, see what comes out of this thing. Hopefully, we got everything we need. It will do its thing. And and, that's not gonna pick on Wrike. Yeah.\u003C/p>\u003Cp>I don't know what's going on with this thing. Potentially some kind of caching issue. Don't know. Anyway, response zero. Down to the wire, five minutes, four minutes, three minutes, two minutes, no minutes.\u003C/p>\u003Cp>Is the server running? Use async data. And what if we just use fetch? Response. It's gonna be response dot letter.\u003C/p>\u003Cp>Oh, boy. Response fetch, browse, params, username. Come on. Failed to stringify the server logs. What is going on?\u003C/p>\u003Cp>This feels like a crappy way to end this one. It should be, like, festive with holiday cheer. I don't understand what is going on with Nox. Oh, duh, dummy. You have to wait the promise.\u003C/p>\u003Cp>Is that getting us what we need? Still not getting us what we need. Data is not defined on the instance. Where else am I getting the data at? Fested messages, data.\u003C/p>\u003Cp>Oh, if response. We're not even getting a response. SSR, undefined, undefined. Hey. That's the way the cookie crumbles sometimes.\u003C/p>\u003Cp>I'm not sure what I am doing wrong with this. I'm sure it'll come to me right after I get done with this. Is it like a key? Cash. Cash, no cash.\u003C/p>\u003Cp>Cache. No cache. No cache. Well, at least Ben's works. At least mine works.\u003C/p>\u003Cp>Not entirely sure what's going on with this little API that I've written, why it is caching this. But, hey, that's AI letters to Santa. That's the way it goes. This has been a hundred apps, hundred hours. Thanks for joining me.\u003C/p>\u003Cp>We'll catch you on the next episode. See you.\u003C/p>","Alright. Alright. Alright. We are back with the Christmas edition of 100 apps, one hundred hours. Today, we are going to be building AI letters from Santa. I've got my lumberjack style on today. My wife called this lumberjack Jesus earlier, but I digress. We're back for more. The rules of 100 apps, one hundred hours. If this is your first show, we have sixty minutes to plan and build an application, a website, a portal, whatever. Whatever we're building, sixty minutes, no more, no less. And rule number two, the anti rule, use whatever you have at your disposal. And since this is an AI Christmas special, I'm gonna pull out all the stops. So let's dive right in. We're gonna hit the clock here. Fire it up. Sixty minutes on the timer. Go. Alright. So AI letters from Santa. What do we actually want out of this? So I have to admit, I cheated a little bit because I thought about this with my team, and I knew we wanted to do this. I've seen things in the past where you write a letter to Santa, you get something back in the mail, etcetera, etcetera. With AI, we could take this up a notch. So combining two ideas. A while back, I saw a GitHub roast page where you enter in your GitHub profile and it, basically will scrape that and give you a roast of how well you're actually not doing in GitHub. So we're gonna combine that with a letter from Santa. And basically, what we wanna do is, enter a GitHub profile. We're gonna scrape that profile. We're going to send that to AI. So we want LLM analysis of the profile. I'm not sure what we're gonna call that. And then I'm going to bucket people on the open source naughty or nice list. So score naughty or nice list. And then we're gonna generate a letter from open source Santa. Generate a letter from open source Santa to that GitHub profile to that profile. Alright. So as far as that functionality, this looks pretty good. Right? What are the tools that we're gonna use of the trade today? I've got a Directus Docker container up and running locally. Directus is obviously the back end we're using to store all of these things. And if everything works as intended p m p m g. I guess, sometimes things don't work as you intend. I've got a Nuxt application that we are going to try and use here. I'm not sure what's going on, but let's hop into the Directus instance. So I'm just gonna pull up Chrome. We'll log in to 8055, and I should be able to pull up my back end. So great. Got Directus running. You could see this is a pretty blank instance of Directus. This is just the boilerplate I use now. There are a couple extensions installed that I was testing, just messing around with. But, let's make sure. What are we doing here? For MPMI. Sometimes these things never go as planned. Okay. So maybe now we can get this Nuxt application up and running that will be served at local host 3,000, and we'll just basically use it to scaffold out our communications. As far as what I'm using, I've upgraded this boilerplate that I've used for 100 apps to, the Nuxt UI v three alpha, just to play around with Tailwind four and, you know, some of these nice new components that are coming from, like, Radix view. So alright. Let's actually model this thing out. Right? What do we need as far as our data models? I think we just need, like, a maybe like a profiles. So under profiles, we would have, what, our username, letter from Santa, letter from Santa, list, you know, are you naughty or nice? Great. And what else do we need? Let's let's jam on that. So we'll, just set up the back end for this. I'm just gonna create a new collection. We're going to call this profiles is the name of it. And why can't I zoom way in? There we go. That's maybe too far, but all good. Let's do created at, updated at. K. Status sort not needed. Who this was created by, I'm not super concerned with. So now we have a profile. We're gonna do the username. Great. That's where we'll store the GitHub profile. What else do we need? What else did we have here? We've got the letter from Santa. What is that gonna be in letter from let's just call it letter. Great. We'll use the WYSIWYG editor inside Directus so we could just store, I'm assuming, HTML content for that. And then we've got the list. So that's basically gonna be a string. We can, you know, make this look nice inside the directus admin. We'll just give it a naughty. Feel naughty just typing that out, and then we have the nice list. Great. There we go. And I'm just going to go on record that we're probably as soon as we start typing naughty into the AI stuff, we'll probably get some some things back. Like a I probably set off the content alarms or something like that. So there we go. We've got a username. We've got a letter. We've got a list. You know, I could potentially put that in here. What I'm gonna do now, I'm just gonna let's just we're just using Directus to store this at the moment. Right? There's a lot of different ways I could go with how to actually generate the application here. But Directus allows me to create custom extensions. What I'm gonna do here is just start, working on this from the Nuxt side of it. So we're gonna input the, actual form here. Let's add a profile. What are we gonna call this? Let's just call this letters dot view. We'll get a view component set up. Lang equals TS. Great. I need to work on my little macros here. Okay. Alright. The other thing that you'll notice here is that I am using cursor. So cursor I recently started testing this thing out. Really enjoying the actual auto completions for this thing. So, I don't have it usually generate like a a giant list of code, but the automations, or the auto completions are are pretty nice for this thing. So let's start with, what, step one. It'd be enter GitHub profile info. GitHub username. Alright. So the only thing here, sometimes it gets a little wonky with the okay. So we use the you form from Nuxt UI. Good question, Brian. The new one, the alpha, they changed some of the conventions. So I've got a form with a schema. I've got a form field instead of a form group, and then I've got an input. Okay. So we've got the form, new form field, and input GitHub username. Let's just see what that gets us on the front end. We're gonna go to this page, which is letters. Okay. Alright. So let's go ahead and just center this up. I think there's actually a container component we can use. Great. Cool. Okay. So now we have a GitHub username, and let's add a submit button. New button, click handle submit, and boom. We have a GitHub username, blah blah blah. Hit submit. Supposedly does something. What it's gonna do right now? Absolutely nothing. Alright. So the next thing that we wanna do, let's kick this thing off. We want to have the form state, we use reactive for that. Great. GitHub username. Okay. And then we're gonna write a function to handle submit. Thank you. Yeah. Great. We'll just, console log that. Right? Boom. There we go. We can see the GitHub username, yada yada yada. Alright. This is actually gonna be an async function. Great. Okay. So now what do we wanna do with this? Right? We have to think about our application structure. And what I'm gonna do here is just basically add a Nuxt server route. So if we break this down, in this server route, what we're gonna do, call the GitHub API, call GitHub API. We're gonna wanna grab a couple pieces of information like the user profile, or any other repos, and maybe, like, their their public read me. I guess we could loop through the actual repos and, you know, pick up more information there, but, let's see what we can get done with that piece. Alright. So let's just go here. We're gonna set up a new route. Let's call it, roast route. We'll do post, and just gonna copy the event handler here. K. So now whenever we hit this route with a post, it should return hello world. We can just check and see if that's actually gonna work. So, we will do the I'm trying to think if that's gonna return. Nope. So we got the response. We're gonna do await. We can use the regular fetch or the dollar sign fetch, which is the Nuxt specific version. And just test this out, see what we get back in the console. Where are you? Okay. Yeah. So we can see the request going out. We can see hello world coming back. Great. Cool. Alright. So now what we're gonna do, right, let's just scaffold this out. We are going to pick up the body. There's a wait read body. Great. So that is going to have the GitHub username in there. And, how did we spell that? Yep. Great. Alright. So we're gonna say GitHub username, and then we've got, like, this git roast function. I'm not really sure where some of these auto completions are coming from. But, what we're gonna do next, let's call yep. There we go. That's a good one. API users, GitHub username. Is that the correct one? Let's just test that. All the developers on my team are screaming and crying at the moment, watching all these AI auto completions. So that seems fair. Great. And let's actually use the Nuxt equivalent. Just this is using OFETCH, which does some automated data transformation and should automatically throw errors for you as well, which is nice. So this is gonna be the let's call this a profile. Alright. And then if we take a look at the profile, we probably wanna get the actual repos for that user as well. Alright. So we'll get the repos. Great. And let's just take a look at the data we're getting from the actual repos. Okay. So what do we actually concern ourself with here? Do we actually want all of this information? What do we actually care about from these? So stargazers, watchers counts, maybe those properties. You know what? Let's just jam it all in there and see what comes out of it. Right? And then let's get the profile readme. GitHub user content, GitHub username, repos dot name, Repos dot name. No. That's not gonna cut it. I think it's gonna be, what, GitHub username. GitHub username. Somebody who's already done this before. Give me the structure. And then main. Let's just see if we can find that. Read me will just populate my name. I don't even know if I have a actually have a read me. Yeah. There we go. Okay. So that is the structure. Great. That is what we needed to confirm. And now let's just actually return this and see what we'll get back. Alright. Brian Gillespie. Now I'm gonna fire this away. Roast. No. Nothing found. Well, that's a little concerning. GitHub username equals body. Read the body. We have fetched the user's GitHub username. Let's just console log the username. API dot GitHub users. I don't see the actual username coming back. GitHub. That's always fun. Alright. What did I do wrong? GitHub underscore username. Okay. Oh, duh. Are we actually passing that in the body of the form? Form. Console dot log response. Request payload. API slash roast dot post. What are we getting back here? GitHub username form dot GitHub username. Oh, that's right. We are missing a state variable here. So is it actually submitting the form? No? No. Okay, friends. What do we do from here? We have handle submit. We're going to use fetch await. Wait that fetch request. We should have already got this back. Of course. There it is. What a dumb dumb. Forgot to actually fix the v model there. So that's what that is. Sometimes, these are not great to do at the end of the day. But okay. Where we at as far as time? We've got forty two minutes remaining. I feel pretty confident on this one. Alright. Now with our roast, we can remove this. We should be able to get that information. Now let's make sure that we're getting what we want back from that API. Great. There's the profile. There's the repos, and there's the profile. Read me. Great. Alright. So what are we gonna do with these now? Right? The next step in this process would be to, pass the profile to LLM and ask it to summarize for us. What do we want this to return? It should return something that looks like this. We want a letter from Santa. Letter from Santa in HTML. And then we're gonna want the the list, naughty or nice. And that should be all we really need to return. Alright. LLM returns JSON. Cool. So what is the LLM we're gonna use? You know, typically, I use OpenAI for a lot of the stuff that I do here at Directus. I've been messing around a lot with, Claude locally. So we're just going to try this out. Santa letters. So we're gonna use anthropic. There's my API key. We're going to drop that in our ENV file, if I can actually get there. Jeez. There we go. I'm just gonna call it Claude ABI key. Okay. Great. And by the time you've watched this, hopefully, I've disabled that key. So, don't stop the video and try to figure that out. Alright. Let's pull up our docs for the API. We need to get the API reference. And let's define this prompt. Prompt. You are a letter writing AI. Alright. Analyze the following GitHub profile. You are the open source Santa Claus. You determine whose open source contributions are naughty or nice, analyze the following GitHub profile, Return a JSON object with the following fields, a letter from Santa and HTML. Set a really high bar for the nice list. What else do we need as far as a prompt? And, yeah, here is the data, profile, readme, json, stringify. Wonder why it's doing that. But okay. Nevertheless, there we go. Turn a JSON object instead of really write the letter in a snarky sarcastic tone. Cool. Alright. And now we're going to send that to Anthropic. Alright. So if we look at their oh, looks like we could just use their JavaScript SDK. That's great. Let's go ahead and open this up. We'll fire that up, install this thing. Import anthropic. Great. And then we're going to create that message. Alright. Constant AI response equals anthropic messages dot create Claude Sonnet. Okay. Messages user role, content prompt. Do we wanna set, like, max tokens? What is the what's the default for max tokens? Where do we actually pass this API key? Getting started authentication, x API key. API key equals process e n v. And, again, like, you could start to see why I really like using cursor because it has, like, this sixth sense for a lot of this stuff that I'm actually trying to do. Sometimes it gets that wrong, but a lot of times it gets it right. So alright, AI response messages. Do we wanna set a max tokens? Body messages, max tokens required. Let's give some more parameters. Write a short letter in a short in a snarky sarcastic tone. That is 500 words or less. And then for the tokens, if we look at Sonnet, we've got like a context window of like 200,000, so maybe a hundred thousand tokens. Oh, no. That's the output. Max output is eight nine one two. That's fine. Max tokens. Great. And let's return. Actually, what we're gonna do next is save that to the Directus database. Right? So we've got this collection for our profile. What I've also done, I've got a utility set up here. This is just using the Directus SDK. And one of the nice things about Nuxt, I say that a lot, is, the ability to it will auto import this for me. So I don't have to import it. I should just be able to call Directus server right here. So let's call it Directus response equals await directus server dot request create item. That's going to be in the profile. And we'll do the GitHub username. That's actually going to be username. Letter response, content dot text. I don't actually know what we're gonna get back directly. Return only a JSON object. And maybe we wanna add something like this for let's just do code. We'll set this up. And I'm just gonna add a field for, let's call it metadata or something where I'm just gonna store the entire response. And honestly, let's just do that to begin with. Metadata, AI response, content dot text. So if we take a look at the API reference, we go back to messages here. I'm kinda curious as to what we're gonna get back. The content text. Okay. Type text something. We'll get back something from the system. Let's just even do it this way. We'll say content direct us response, and then we're going to return direct us response. See what that gives us. Now let's go in. Where's our app? We'll switch back to Chrome. I do like Arc. I've found it to be lacking for development because it's just not super fast. Alright. So fingers crossed that this actually does what it should do. And, let's make this even nicer. And we'll add, like, a loading state, constant loading, ref equals false. We'll add loading dot value equals true. Loading dot value equals false. Great. And what else do we want to do? Is there a loading state on the actual form? Let's take a look. So Nuxt UI state, there is not a loading state on that. There should be on the button though. So just update that. Okay. And let's test this bad way out. Submit. Alright. We're waiting. We're waiting. We're waiting. We're waiting. We're waiting. This could take a minute. So, you know, we might even want to, like, potentially set up a oh, okay. So we're not getting anything back. We see a request error. So let's go into our roast, and we should probably do some error handling. Alright. Catch error, console error. Return, or we could just throw the error. What do we got here? Format. Alright. Let's refresh. I'll try this again and see what kind of error we're getting and why. Pending. Invalid user credentials for Directus. Okay. Great. So, just wasting tokens there, throwing them into the void. One of the things that you'll notice, I do have this direct as URL set up, but, my server token is probably a % not correct. So I'm gonna go in and create a token for this. We'll just create a new token. We'll call this the server token. And I wanna make sure in my utility that I have that set as server token, direct us URL. Okay. Let's try this thing again. PPM dev. I will restart the dev server, pull in that new ENV, though I think Nuxt may automatically update that for us. How we doing on time? We got twenty nine minutes left, so I'm feeling pretty confident that we can get something out of this. Let's go ahead and try it again. Bryant Gillespie. Submit. K. K. Roast. You do not have permission to access this. Okay. Can anybody spot the error? It is because I left off a s. We have profiles, and this is profile. So again, if I I don't know if I you can actually see the logs for anthropic. Okay. Yeah. We could see here's the actual logs. It's probably not showing what we've got there. But anyway alright. We'll try this one more time. Let's just clean this up a bit. And away we go. Dun dun dun. I don't like the looks of this, actually. Let's just reset. Try this again. AI response. We got the prompt. Got the profile. Dun dun dun. The moment of truth. Are we actually gonna be able to get this thing to work? Brig Gillespie. Submit. Obviously, this would probably be better as, like, a background job or something like that. Alright. So we refresh, and we have something here. Okay. Yeah. So we're getting some text back. It looks like we need to parse the JSON. The letter is going to be text parsed response, text, parse response dot list, and then we get metadata, which would just be the parse response, I'm assuming. Alright. We're gonna delete this out. Let's run this again. And hopefully I'm not burning through all these credits that I loaded up. Okay. So now we're looking great. Okay. So we have our username. We've got our letter. Ho ho ho. What do we have here? Another developer thinking they can impress Santa with a few measly repositories. I've seen l's with more impressive profiles. I got made it to the naughty list. Great. Amazing. Alright. So now that's working as intended. Let's let's make this pretty. Right? The form, we're going to do max width. Maybe we set this to Excel. Move that form to the somewhat in the center of the page. And let's just lean on AI here. Right? This is already pretty cool. One of the other things I wanna do is maybe we set up a route where we actually surface this letter. Right? So if we do let's do letters as a directory inside pages. And we're gonna do the username in brackets. So just take this username, make that in brackets, and then I'm gonna put letters inside here, and we'll change the name of this to the index route. Alright. So let's just clean this up a bit, wrap this up, and console the error loading. Actually, we could do that in finally. Great. And what we're gonna do, if the response is good, we could navigate to the username page. Cool. And that way, you know, basically, like, this could get very expensive if if you made this thing public. Right? You don't want people generating like 35 letters to Santa. So we can add a check to the database if we've already got that GitHub username and just return the letter that we we already have. Right? Okay. So on the response, as long as there's no error, we're going to navigate dot to form. Github username. And this would be await. Navigate to. Cool. Alright. Now let's just lean on AI and see what we could do. Add some Christmas theming to this. Let's see what this actually will do. Add some Christmas thinging, ho ho ho. Looks like it's generating some random messages. Code review letters to Santa, random message, decorative elements. Great. Love decorative elements. Now, with cursor, I'm just gonna click apply here. It should go through and run through this actual code. I can close this out and see, you know, in kind of a preview way what it's gonna change. And if we hit reload oh, what we got going on here? Letters index. Is that because I changed the route? Okay. Yeah. Now we're looking very festive here. This looks this looks great. AI, what can you do? Alright. The other thing I see, maybe we want this to be block. Will that get it done? Block. Class. Let's just make the width full. Width full. Okay. And then let's shrink this actual form a bit. Yeah. MD. There we go. Alright. We're deep in the Christmas cheer now. And, while we wait, let's well, not while we wait. Let's actually go in and now we're gonna work on this letter. Alright. So, this does have a Nux plug in. This is just my boilerplate where I can go in and actually request the information from Directus on the client side, or, you know, I could set up a route for this on the server side in Nuxt. Both of those ways are are totally valid depending on your application. Obviously, totally up to you. We will just, let's let's keep it the same theme. We're going to, like, fetch roast, or we could do, like, a roast.git.ts. And what are we gonna pass? Do we want to pass the username as a param, or we'll just pass it as a query parameter? Okay. So in this one, what we're gonna do, we will call the profiles endpoint inside Directus. So we'll just go const, response equals await Directus server. And, you know, sometimes you wanna make requests on the server side. That's why I've got this set up, this way. We're gonna do read item, and I gave this a UUID. We could've used the actual profile as the primary key. But, what we're gonna do, read profiles, and we're gonna set up a query for this. So we'll do a filter parameter, the username. So that's the field. We're gonna drop down again. This will be equal to the username. So first we're gonna have to get the username equals get router param. Nope. We're gonna do git query, and that would just be the query. Great. Username. We could destructure this if we wanted to. Return username equals username, and we're gonna return that response. Great. Cool. So now we do this. And on this one, what we can do is use the use fetch composable from Nuxt. So this will be we've got some data. We're gonna use fetch, and we're gonna call API slash roast. And the is it params? I believe. See what we got. And let's add the so the same festivities, I guess. Festiveness. Perfect. Alright. Decorative elements, blah blah blah, random messages. We're gonna put that up here in the script. Okay. Code review letters to Santa. And instead of the form, right, we're gonna replace this with data. Alright. So now if I do this, what's gonna happen? Route is not defined. Okay. So we just need to call use route to fetch that route. And do we actually get the stuff that we need here? We could test this API as well. Letters API roast username equals Brian Gillespie. Okay. Yeah. So that's getting us what we want from direct us except is it query? What is the use fetch? This is where, like, Nuxt documentation comes in handy. Use fetch. Where we at? We got sixteen minutes remaining. We got data use fetch. What are the URL query? Okay. Alias for query. That's what I thought. Root params username. Oh, data. Are we actually let's jump into the view dev tools. We'll hit the username route. And I see the data here. Here's the issue. Right? It is returning an array. So inside our routes, we could, you know, do something like this where we're just picking off the first item. I could also do that transform that on the the Nuxt side if I wanted to. Here's our letter from Santa. Cool. Code review letters from Santa. What I'm gonna do, let's use the pros class from Tailwind to get styling for this. We'll make the text dark green. Great. That's fine. And then the interior of this, we're just going to use v HTML. So we get this. Do I not have Tailwind typography into this? At plug in Tailwind typography. Okay. Yeah. So there we go. Now we've got the letter from Santa Claus. This is looking really nice. Perfect. Let's add like a cursive font. Right? Font family cursive. And this is Tailwind four, where all the config is basically CSS variables. So, really enjoying that Nuxt module, playing around with it. Let's find a handwritten font. Okay. Caveat. Looks nice. Nuxt has also added a a like this font amazing thing where you just throw your fonts in the CSS and it will actually download these things for you. So let's take a look at this. Right? I'm just gonna change this to font cursive and bada bing bada boom, we get what we want. So let's put, like, pros XL to XL. And there we go. So dear Bryant, what do we have here? Blah blah blah, etcetera. We have got thirteen minutes left on the clock. What can we do for fun? Let's go back and actually test this thing out. I'm just gonna refresh. There we go. I'm gonna do our fearless leader here at Directus, mister Ben Haines. We're gonna send this to Santa, and something bad happened. We could not find okay. So it looks like this one is not finding Ben's profile. Haynes, Maine. And Haynes Haynes Haynes Haynes Haynes. Would that be at, like, Master branch maybe? Where's our roast? Roast.post, profile read me. Try. I bet it's at master. I'm just gonna do this the quick and dirty way. Alright. So we go back. Let's try this again. Mister Ben Haines, we're about to roast you, sir. Alright. So we're checking the list twice. And eleven minutes on the clock. We've got the letter to Ben Haines from Ben Haines. Why are we not seeing the actual letter? There it is. I'm dreaming of a Nuxt application that actually works. What is going on with this? Letters, username, data dot name. I'm assuming because there is no name. Username. I'm running this on a sour note here. Ben Haines. That's kinda weird. Ben Haines. Why is it doing that? API roast username. What is going on here? Get async data. API roast. Why does it work for me and not for mister Haynes? What are we actually doing wrong here? Did I spell the name wrong? GitHub username. Alright. E pipe. Use fetch roast. Can't find the username? Profiles get username, get query. Is it read query? No. It's get query. Return query. API API roast. Ben Haines. So why aren't we why isn't this working? So it's not actually finding the username for that, which is odd because I have the username right there. Filter contains. Okay. I don't understand it, but we're gonna roll with it. Great. Some type of encoding or something maybe. Not sure. Booyah. Ben, I'm gonna read this to you. Dear Ben, ho ho ho. Well, isn't this embarrassing? I've been reviewing your GitHub profile, and I must say I'm thoroughly underwhelmed. 20 whole repositories, you must have been super busy this century. Meanwhile, Santa's got billions of believers worldwide. Look, I'm not saying you're on the naughty list because your contributions are lackluster. I'm saying if you were open source for coal, you barely have enough to heat a dollhouse. That is brutal. So let's call that a win. This is AI letters with Santa. Do we wanna do one more just for fun? Just for giggles? Let's let's test this out. Directus Directus. I forget Reich's actual, GitHub profile. There it is. Okay. So we're gonna throw mister Reich Van Zanten in there, our CTO, see what comes out of this thing. Hopefully, we got everything we need. It will do its thing. And and, that's not gonna pick on Wrike. Yeah. I don't know what's going on with this thing. Potentially some kind of caching issue. Don't know. Anyway, response zero. Down to the wire, five minutes, four minutes, three minutes, two minutes, no minutes. Is the server running? Use async data. And what if we just use fetch? Response. It's gonna be response dot letter. Oh, boy. Response fetch, browse, params, username. Come on. Failed to stringify the server logs. What is going on? This feels like a crappy way to end this one. It should be, like, festive with holiday cheer. I don't understand what is going on with Nox. Oh, duh, dummy. You have to wait the promise. Is that getting us what we need? Still not getting us what we need. Data is not defined on the instance. Where else am I getting the data at? Fested messages, data. Oh, if response. We're not even getting a response. SSR, undefined, undefined. Hey. That's the way the cookie crumbles sometimes. I'm not sure what I am doing wrong with this. I'm sure it'll come to me right after I get done with this. Is it like a key? Cash. Cash, no cash. Cache. No cache. No cache. Well, at least Ben's works. At least mine works. Not entirely sure what's going on with this little API that I've written, why it is caching this. But, hey, that's AI letters to Santa. That's the way it goes. This has been a hundred apps, hundred hours. Thanks for joining me. We'll catch you on the next episode. See you.","ec46771b-85fb-4967-9a7e-81102f96cf74",[183],"cc9262db-1ae2-4bc1-a1f9-7d2fc51a396d",[],{"reps":186},[187,243],{"name":188,"sdr":8,"link":189,"countries":190,"states":192},"John Daniels","https://meet.directus.io/meetings/john2144/john-contact-form-meeting",[191],"United States",[193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242],"Michigan","Indiana","Ohio","West Virginia","Kentucky","Virginia","Tennessee","North Carolina","South Carolina","Georgia","Florida","Alabama","Mississippi","New York","MI","IN","OH","WV","KY","VA","TN","NC","SC","GA","FL","AL","MS","NY","Connecticut","CT","Delaware","DE","Maine","ME","Maryland","MD","Massachusetts","MA","New Hampshire","NH","New Jersey","NJ","Pennsylvania","PA","Rhode Island","RI","Vermont","VT","Washington DC","DC",{"name":244,"link":245,"countries":246},"Michelle Riber","https://meetings.hubspot.com/mriber",[247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,224,435,436],"Albania","ALB","Algeria","DZA","Andorra","AND","Angola","AGO","Austria","AUT","Belgium","BEL","Benin","BEN","Bosnia and Herzegovina","BIH","Botswana","BWA","Bulgaria","BGR","Burkina Faso","BFA","Burundi","BDI","Cameroon","CMR","Cape Verde","CPV","Central African Republic","CAF","Chad","TCD","Comoros","COM","Côte d'Ivoire","CIV","Croatia","HRV","Czech Republic","CZE","Democratic Republic of Congo","COD","Denmark","DNK","Djibouti","DJI","Egypt","EGY","Equatorial Guinea","GNQ","Eritrea","ERI","Estonia","EST","Eswatini","SWZ","Ethiopia","ETH","Finland","FIN","France","FRA","Gabon","GAB","Gambia","GMB","Ghana","GHA","Greece","GRC","Guinea","GIN","Guinea-Bissau","GNB","Hungary","HUN","Iceland","ISL","Ireland","IRL","Italy","ITA","Kenya","KEN","Latvia","LVA","Lesotho","LSO","Liberia","LBR","Libya","LBY","Liechtenstein","LIE","Lithuania","LTU","Luxembourg","LUX","Madagascar","MDG","Malawi","MWI","Mali","MLI","Malta","MLT","Mauritania","MRT","Mauritius","MUS","Moldova","MDA","Monaco","MCO","Montenegro","MNE","Morocco","MAR","Mozambique","MOZ","Namibia","NAM","Niger","NER","Nigeria","NGA","North Macedonia","MKD","Norway","NOR","Poland","POL","Portugal","PRT","Republic of Congo","COG","Romania","ROU","Rwanda","RWA","San Marino","SMR","São Tomé and Príncipe","STP","Senegal","SEN","Serbia","SRB","Seychelles","SYC","Sierra Leone","SLE","Slovakia","SVK","Slovenia","SVN","Somalia","SOM","South Africa","ZAF","South Sudan","SSD","Spain","ESP","Sudan","SDN","Sweden","SWE","Tanzania","TZA","Togo","TGO","Tunisia","TUN","Uganda","UGA","United Kingdom","GBR","Vatican City","VAT","Zambia","ZMB","Zimbabwe","ZWE","UK","Germany","Netherlands","Switzerland","CH","NL",1773850450565]