In this chapter we're going to continue working on our knowledge base which we started in the previous chapter by learning how to generate embeddings. So in this chapter we're going to start by creating our files.list function and then we're going to create the UI. That's going to be this knowledge base table right here and then we're going to create the UI. That's going to be this knowledge base table right here and then we're going to create this upload files dialog as well. We're also going to enable the UI functionality to delete files.
Let's start this chapter by double checking the embeddings dimension number. If you remember in the previous chapter when I ended the chapter, CodeRabbit left a comment here to standardize my embedding dimensions. And this actually let me think, did I tell you the correct information to use the same number as I did because I'm using open AI and I don't know what you are using. So I asked it, where can I find info on other embedding dimensions? And it basically gave me all of these options.
And it actually gave me a pretty comprehensive snippet here. So if you're using OpenAI, you're using the same number as me. If you're using Google, you should use this number, apparently the same as me and this one. If you're using grok it's another very specific number. And for example if you're using anthropic you don't even have embedding models available.
So what I want you to do to start this chapter is Just double check that you're using the correct embedding number. Let's go inside of our packages, backend convex and let's go inside of systemairag. In here find the model that you're using and your provider and you can either use, well this snippet that CodeRabbit generated for us to find your dimension, or head to AI SDK. You can see the link on the screen. Find your SDK provider here, for example, Google Generative AI, scroll down, find embedding models here and when you scroll a little bit down you will find what default dimensions are for what model.
So depending on what you're using make sure that you are adding the proper dimensions here so don't just blindly use the same model, the same number as me. Like that. Great. After you have established that let's go ahead and focus on files.list. And we can double check this once again when we actually add the search tool to see if our embeddings work or not.
Now let's create the files.list function. So we're going to go inside of private files.ts and at the bottom here let's go ahead and do list like this. And let's go ahead and accept the following arguments. So we're going to accept the category if we ever want to query by category and we're going to add pagination so we can just import these from convex server and we can import query from generated server same as action and mutation. Now let's add the handler which is an asynchronous method And let's go ahead and add arguments and context.
Whoops, opposite direction, context and then arguments. And first things first, let's check if we are logged in and if we have the organization. So I'm going to copy this and this and I'm just going to paste it here. And then after we've confirmed that the user is logged in and has a valid organization, let's go ahead and let's obtain the namespace. So const namespace will be await rag get namespace context namespace is organization ID.
Now in case we couldn't find any namespace, let's go ahead and let's just return an empty query. So return page empty array is done true continue cursor empty string. So this is basically the format that pagination returns, but in case we can't find the namespace, let's just throw an empty array so we're just going to display nothing was found. Now let's go ahead and let's find the results from our rag. So const results await rag.list context namespace ID will be namespace from the constant above dot namespace ID and pagination options will be arguments pagination options so pagination is required here and now Let's go ahead and let's convert all of those files that we just found into readable format because these are entries from embeddings and now we have to convert those to files we can add in our table.
Results.page.map and then for each entry that we found, we're going to use our method convertEntryToPublicFile and pass in the context and entry. So by default this is just an error because we don't have this yet. Let's go ahead and quickly create it. Asynchronous function convertEntryToPublicFile is going to accept the context and this will be a query context type which you can import from here. And you can actually use pick and just choose storage.
That's the only thing we need. And for the entry let's go ahead and use the type of entry. We can import that from convex dev rack. And let me go all the way down okay I will just scroll here great now let's go ahead and let's define the return method here So the return method will be a promise. And inside of this promise we're going to define a public file.
So let's export type public file from here. ID will be entry ID. We can import this from convex dev rack or maybe we already had it imported already. Just make sure you have it. Name will be a string.
So this will be what we are converting to, right? So name will be a string, type will be a string as well, size will be a string, status will be ready, processing or error, URL will be string or null, category will be an optional string. And then you can use the public file type here as the return method. Now let's go ahead and let's also define the entry metadata while we are here. I think we don't have it here.
Entry metadata. We don't. So let's do type entry metadata and let me just confirm. Is there any other place I might have used this already? Entry metadata.
I have not. All right. Let's go ahead and then define it here. Entry metadata will have storage ID which is ID with a type of storage. Uploaded by which is a type of string, file name which is a type of string, category which is a type of string or null.
We'll see if we need this for this method. I'm pretty sure that we do but I also feel like we have written this type somewhere because this is the metadata type but I couldn't find it anywhere. Let's see maybe I call it something else in a different file. So let's go ahead now and continue developing our convert entry to public file here and let's do const metadata entry.metadata as entry metadata or undefined. Then let's define the storage ID here to be metadata?storageID.
So it's important that your metadata here matches exactly the way you store your file here. Right? This metadata right here. So if you want to, maybe you can somehow add asEntryMetadata like this. So you don't have any errors here and so that you know that everything works fine.
I think that could be a good idea. Maybe add this to your rag dot add because I don't think it will change the way this function works, but I do think it will help you in showing any errors if you have something incorrectly written here. So this way I think this is a bit more secure. Now let's define the file size and by default it's going to be unknown. If we are able to find the storage ID from the metadata, let's go ahead and try and extract it.
So, storage, metadata, is await, context storage, get metadata, and pass in the storage ID. Now let's see. So this is now deprecated. It says use database system dot get. Interesting.
Let's see. So database. Does it mean like this. Okay. Maybe I need the entire type here.
Let's try database dot system dot get and then I just pass in the storage ID. Very interesting. I think that is the same thing. We'll see. So if I have storage ID, my apologies, if I have the storage metadata, I'm going to go ahead and do file size and let's go ahead and do format file size and that's in storage metadata dot size Now we have to develop the format file size.
So I'm going to do this just below here. Function format file size accepts the bytes, which is a type of number, and returns a string. If bytes is equal to zero let's simply return zero bytes. Otherwise let's define a kilobit here. Let's define sizes.
Let's define byte, kilobyte, megabyte and gigabyte. Let's go ahead and find a constant I to be math.floor, math.log bytes divided by math.log divided by our k constant here and let's go ahead and let's return inside of backticks number parse float open a function, open another parenthesis inside, bytes divided by k times I to fixed 1. And then let's go ahead and choose the size. So this will be sizes. There we go.
So there's probably a library for this but inside it does the exact same thing as this. Now let's go ahead and once we have this file size, let's do the catch here so if we have an error, let's console error failed to get storage metadata and let's simply forward this error so we know what's going on. Now let's extract the rest of the file info so const file name here is entry.key let's see where do we have the entry here is the entry All right, we should have it then. Entry.key or unknown. Then let's define the extension to be filename.split pop?2 lowercase or text.
Now let's define the status which can be a type of ready or processing or error and by default let's set it to error. If status, my apologies, if entry.status is ready, let's return, my apologies, let's set the status to be ready. Else if entry.status is pending, let's set the status to pending, My apologies, to processing. Now let's go ahead and let's get the URL of the file. URL, check if we have storage ID.
Await, context, storage, get URL, storage ID. Otherwise, use null. And finally, let's return ID, entry, dot entry ID, name, file name, type, extension, size, file size status url category metadata question mark category or undefined and if you've done this correctly you shouldn't have any errors in your code. Now we have developed the convert entry to public file and now all of our files here will have that specific public file format with id, name, type, size, status, url and optional category which will help us in displaying it in that table and now let's go ahead and filter them by category if the category ends up being provided so filtered files will be arguments.category if we have it we're going to do files dot filter file file dot category matches the category from our arguments otherwise just return the files and Now let's return page, filtered files is done, results is done, continue cursor, results, continue cursor. There we go.
Now we have the function to fetch our list of our files which we have embeddings for using the safe namespace and by converting them to public files as well as providing the size of each file just so we can know more specifically what file we are talking about here. So as for this function here, the reason I didn't explain it too much is obviously because I didn't write this function myself. I basically found one of the packages that exist and with AI I just created my own little file here for a simple reason. It's super simple. And I think adding a whole package just for this kind of makes no sense, right?
We can just do something like this. And we'll see CodeRabbit review this if there's an obvious issue here. So now let's go ahead and let's use these files. So I'm going to go inside of apps, inside of web, app, dashboard and let's go inside of files, inside of page.tsx. Let's keep this open and let's go inside of modules and let's go ahead and create new files here.
Module. Let's create UI and let's create screens. My apologies, views. And inside let's create files view.tsx mark this as use client export files view component files view Now go back to page here and simply return File as View. As simple as that.
Now make sure that you have your app running, specifically web. Let's go to localhost 3000 here and let's check out if everything is working. So we should now be able to go inside of our sidebar, click on knowledge base and it should say files view. Perfect. Now let's go ahead and let's develop the files view.
So what we're going to have to do here is we're going to have to import all the components from our table. So table, body, cell, head, header, and row from workspace UI components table. While we are here, let's also import all the infinite scroll components because we know we're going to need them so this includes the infinite hook and the infinite trigger. Then let's also prepare the paginated query from react my apologies from convex react This will help us fetch the files that we just created. And let's also prepare the API.
Let's see. So workspace backend this cannot be found oh because it's like this yes my apologies and let's also import type public file from workspace backend private files So we have that type public file that we are returning. Instead of this files view, let's start by adding const files and let's do use paginated query, api, private, files, list. And let's leave the argument empty. And initial number of items is going to be 10.
Now inside of here, let's go ahead and let's do json.stringify and pass in the files. And as you can see we have no files at the moment because we didn't even add any. So now let's go ahead and let's start developing this component. So I'm going to give this div a class name of flex minimum height of screen flex column bg muted and padding of 8 bg muted there we go now inside of here let's go ahead and let's limit how wide this will go so let's add a new div Let's give it a class name mx-auto-full-width-maximum-width-screen-md. Let's go ahead and open up div here with a class name space y2 Let's add an h1 element knowledge base Let's give it a class name text to excel on medium text for excel Below it let's add a paragraph upload and manage documents for your AI assistant.
Let's go ahead and give this a class name of text muted foreground. Now let's go ahead and separate this and let's give the div below it a class name of margin top 8, rounded large, border and let's add bg background. Now inside of here let's add the table header. Well, not exactly the table header but a space above the table. With flex items center justify and border bottom px6 py4 and in here let's add button from workspace UI components button and let's add plus icon from Lucid React and add new button.
And let's go ahead and add on click here for now to be just an empty arrow function. Make sure you have added the plus icon. And there we go. Now you have a new add new button. Let's go outside of this div here and let's add our table element.
Now let's define the actual table header. Inside of this table header let's add the table row. Now inside of this table row let's add name with px6, py4 and font medium under the table head component. Now let's go ahead and do the same thing for type. Let's go ahead and do the same thing for size.
And let's go ahead and do the same thing for actions. And now we have name type size and actions here. Outside of table header let's add table body. Now inside of here let's go ahead and first add our infinite scroll because we can already do that here. Just after files let's go ahead and let's add use infinite scroll, status files.status, load more files, load more, load size 10, same as the initial number of items.
Top element ref, handle load more, can load more, is loading more, well is loading first page and is loading more. Like that. Now let's go inside of this table body here and let's do the following. Open curly brackets, open parenthesis and write an arrow function inside and then execute that arrow function. Now inside of here we are going to check if we have isLoading first page we are going to return table row, table cell and loading files text inside.
Let's give this table cell height of 24, text center and call span of 4 because we have 1, 2, 3, 4. So this will take the entire table. Now let's do if files.results.length is equal to zero. In that case we're going to do a similar thing so we can copy this so let's add return and instead of loading files this will be no files found and now let's go ahead and let's actually iterate over our files so return files.results.map find the individual file add table row let's go ahead and add a class name here, hover, bg muted, 50% opacity and key file id. Then let's add table cell here and let's go ahead and give it a class name BX6PY4FontMedium.
Inside let's add a div. And let's go ahead and add a class name FlexItemCenter and Gap3. And inside let's render File.Name and just before that let's add File Icon from Lucid React so make sure you have added File Icon and let's render File Name Now let's go ahead and copy this table cell and then let's go ahead and change this with badge component. So make sure to import this from the proper place. Import badge from workspace UI components badge like this.
Let's give this badge a class name of uppercase and a variant of outline. And inside of here, we are going to render file.type. Let's copy this table cell again. And now we are just going to render file.size. Nothing else.
And let's do a slight little change here by also adding textMutedForeground. We can actually remove font medium everywhere I believe because font medium is the default font I think. I mean for our app, not for all apps. Great! And now let's add the last one, table cell.
Let's just copy the class name here. Let's remove text muted foreground. And In here we will have the drop-down menu. So let's import everything we need from the drop-down menu. So right above the table I'm going to add menu, content, item and trigger from WorkspaceUI components drop-down menu.
Let's scroll down here to our last title cell. Let's open the drop-down menu. Let's add drop-down menu trigger. Let's give it as child prop. Let's render the button inside.
Let's render more horizontal icon from Lucid React. Let's go ahead and give the button class name. Size 8. Let me fix the typo here. Size 8, padding 0, size small, variant ghost.
And then let's close the sidebar, my apologies, drop down menu trigger. Let's open drop down menu content. Let's go ahead and give it a line of end. And inside let's open drop down menu item. And let's add trash icon from Lucid React and delete.
Let's give the trash icon a class name of size4mr2 and the drop down menu item class name of text destructive and on click of an empty arrow function. Great. Outside of the table here, let's check the following. If there is no first page loading and if files results at length are larger than zero, Let's go ahead and let's render a div with a class name border top and inside infinite scroll trigger. Let's go ahead and pass in all the prompts that we have extracted from our hook.
So can load more, is loading more, handle load more and top element ref. So that's our infinite scroll trigger. So by default no files are found, right? Because, well, we haven't added any files. So now what I want to do is I want to develop the upload dialog.
So inside of files UI, let's go ahead and implement components. Instead of components, let's go ahead and create uploadDialog.tsx. Let's mark this as UseClient and in here let's import everything we need. So we're going to start by adding everything from the dialog, the content, description, footer, header, title, and of course the dialog itself. Then let's also prepare the input.
Like this. Let's also prepare the label and let's prepare the button and let's also prepare useAction from convex react and useState from react. Now there is one more component we need to add here and that's the drop zone. Thankfully, if you remember, I told you that we're going to be using kibo UI. It actually has built in drop zone.
But again, we have the same problem. The installation CLI doesn't work in Monorepos for me. So don't worry. Thankfully, source code is available right here but if you want to use the exact same version as me, I have added it. You can see the link on the screen here.
Go into the UI, Components, and find Dropzone. Copy it from here. Let's go ahead inside of our packages, UI, Source, Components, New File, Dropzone, .tsx, paste everything inside. And now let's go ahead and fix some issues. So react-dropzone.
Let's go ahead and do pnpm fui add react-dropzone. And once we add this package, let's see if our errors will be resolved. So we just added react drop zone for the UI component and as you can see no errors visible. Great! Make sure you have this file and save it.
Now let's go ahead and let's import everything from our new workspace UI components drop zone. That's going to be the drop zone itself, drop zone content and drop zone empty state. There we go. And let's also prepare our API from Workspace backend generated API. Perfect.
Now let's go ahead and let's create an interface UploadDialogProps with Open boolean, OnOpenChange which accepts the Open boolean and returns a void and optional OnFileUploaded method. And then let's go ahead and do export const upload dialog. Let's assign upload dialog props, open, onOpenChange, onFileUploaded. Now inside of here let's first assign the add file method using use action. We can do that by adding API private files add file.
Make sure it is use action because add file is an action. Then let's go ahead and add a state. Uploaded files and set uploaded files. Let's use state here and let's go ahead and do the following. We're going to give it a type of file and an array like this.
Now let's add is uploading and set is uploading use state false by default. And now let's go ahead and just define the upload form inside of a state. This is kind of breaking convention from our usual way of defining forms. And if you want to you Feel free to use a React hook form. I just feel like this one is a super simple example so we can have it in state.
Let's do handle file drop here. Let's do accepted files to be a type of file and an array. Inside of here let's get the file here. So the first one, if we have the file, set uploaded files, open the array and add file inside as the only item. If not, upload form file name.
So if we haven't attached a file name, let's go ahead and set upload form, get the previous value or well the current value. And let's update the form by spreading the current value and updating the file name as to what the current uploaded file actual name is. You're going to see what this is used for in a second. Perfect. Now let's go ahead and let's actually return a dialog.
Let's give it onOpenChange. OnOpenChange. This is our prop. Open. Open.
This is our prop as well. Then dialog content and let's give dialog content a class name smMaximumWithLarge. Now let's render the dialogue header, dialogue title, upload document. And now below that, let's add a dialogue description. And inside, let's just add a brief description to describe what's happening.
Upload the documents to your knowledge base for AI powered search and retrieval. Now outside of dialog header here, let's create a div and let's give it a class name space y2. Let's add a label but let's actually use the component label that we imported. Let's add an HTML4 category and let's write category inside. Now let's add input which is a self-closing tag, class name, full width id category on change.
Let's go ahead and call set upload form let's get the previous values so we save them spread the previous values here and change the category to be event target dot value Now let's also assign the placeholder to be for example documentation support or product. Let's give this a type of text and value upload form dot category. Perfect. Now let's do the same thing here so I'm going to copy this div and paste it and I will change the HTML4 here to be file name this will be file name And let's go ahead and just add a little span here. Which will say optional inside of parentheses.
And let's give it a class name of text muted foreground and text extra small. And let's just add a little forced space here between the two. Now change the ID to be file name here as well and instead of category update file name here. And the placeholder here is going to be override default file name. So if we want to call the file something different than what it's called and let's change the value to upload form dot file name.
Now outside of this div let's add the drop zone component. The drop zone is a self-closing tag and let's go ahead and give the accept prop the application pdf. Let's just fix this so okay let me do it like this. Application PDF will accept an array of items which end with dot PDF. If we want to upload CSV files, we are allowed to do so as well.
And if we want to add text plane, we're allowed to do that as well. And here's just another thing if you want to. If you want to add Microsoft Word files, this is the official MIME type for it. It's extremely long. So only if you want it.
I didn't even try adding Word files. You can just Google Microsoft Word MIME type, React Drop Zone and then just add it. Again, You don't need this. I just thought it would be fun for you to see Now it is be it's gonna be disabled if we are uploading Let's go ahead and set the max finalist here to be one and On drop here for now. Let's make it an empty arrow function, source will be uploaded files.
And then it's not actually gonna be a self-closing tag because inside of it we're gonna add drop zone empty state and drop zone content like this. And finally, let's develop the last part here. So let me just see, I think I might have done something wrong here. Yes. So after dialog header, add a div here with a class name, space Y4.
And let that div encapsulate all inputs and the drop zone like this and now let's just indent all of that there we go So now after this div which encapsulates the input and our drop zone, let's go ahead and let's enter the dialog footer like that. The dialog footer will have a button. And the button will be cancel. Let's give it the variant of outline. Let's give it disabled of is uploading.
And let's give it on click here for now to be an empty arrow function. And now let's develop the other button which will be either upload or is uploading button. So it's going to be disabled if uploaded files dot length is equal to zero or if we are currently uploading or if there is no uploadForm.Category value. And let's also pass in on click here for now to be an empty arrow function. And let's check if is uploading.
We're going to say uploading. Otherwise upload. There we go. So now let's go ahead and let's develop these empty arrow functions. Let's start with the handle upload method.
So after handle file drop I'm going to develop const handle upload which will be an asynchronous method. First we're going to do setIsUploading and set it to true. After that I'm going to open try and catch block here. Instead of catch I'm going to log the error like this and inside of finally I'm going to set isUploading to false. Now inside of here let's first get the blob using uploaded files and get the first in the array.
If there is no blob found let's go ahead and simply return the method. Then let's get the file name by using uploadForm.FileName or if nothing was written we're simply going to use the blob name. And now let's go ahead and do await add file so we added add file in the beginning here. Instead of this add file let's go ahead and let's add the bytes. We can do that by using await blob array buffer.
Let's pass in the file name if we want to override it. Let's pass in the MIME type to be blob.type or default to text plain The category will be upload for.category. This will help specify the embeddings And then let's call on file uploaded question mark and execute it. Now let's develop the handle cancel method const handle cancel. This one will be quite simple.
We're going to call on open change and set it to false. We're going to set uploaded files to an empty array and we're going to set the upload form and reset the category and the file name to an empty string as it was in the beginning. And now after we submit let's make sure that we call handle cancel method to close the dialog. Now let's use the handle cancel and all the other methods independently. So in the drop zone here we added an empty arrow function I believe.
Let's find the drop zone. On drop, let's go ahead and call handleFileDrop. It's the method we developed first, I believe. This error that I'm having is I'm 100% sure because of my TypeScript server. So when this happens, what I do is I use Command Shift P or Control Shift P and just restart TypeScript server.
And after you do that, it goes away. So we fixed the drop zone empty arrow function. Make sure onDrop uses handleFileDrop. The cancel button should use handleCancel and this button should use handleUpload. Let me just confirm that I don't have any empty arrow functions here.
I don't. Great! Our upload dialog is now ready to be previewed. Let's go back inside of the files view component and let's modify our return to use empty fragments to wrap the entire app around. Let's indent the entire app and just above let's render the upload dialog like this.
Now let's go ahead and let's define the states here. So I'm going to add const uploadDialogOpen and setUploadDialogOpen. Use state False by default. Now let's go ahead and let's pass it some props. OnOpenChange will call SetUploadDialogOpen and Open will call UploadDialogOpen.
Make sure to import useState from react one thing that we are missing here is onFileUploaded or maybe we actually don't need to do anything here. Yes, I think this is fine. So now when we click on add new here, let's go ahead and do set upload dialog open and set it to true. Let's go inside of our files now. Let me just refresh this.
There we go. So now when you click add new you should have this model open. You should be able to add the category, the file name and upload some files. Now I have actually added some files for you to use inside of my assets folder. So go back to this folder and you will find the knowledge base and inside of here go ahead and choose one.
All of these are AI generated. So for example, you can choose something like this, frequently asked questions, just something that you can easily test. For example, some of your plans here, Basically something that AI can easily learn how to answer. So just download one of these files here. I recommend using the frequently asked questions one and then go inside of here and let's try and add this.
So I'm gonna go ahead and add this here and I'm going to call this frequently asked questions. I will not override the name and I will click upload. And let's see if this will work or not. And just like that you can see this works perfectly for me. So let me check my my back end here actually.
So as you can see my back end had no problems handling this at all. And let's double check that everything is actually OK by going inside of my convex dashboard here, inside of echo tutorial, and I'm gonna go and first check my files. And there we go, you can see that I have my files right here, perfect. But what's more important for me is whether the embeddings were created. So I'm going to switch to the Rack component here and you can see immediately this was completely empty before but now you can see that it has the proper namespace ID and it has the embeddings here ready.
So I'm not exactly sure. There we go. You can see you can find the exact parts of the frequently asked questions embedded in here. Amazing, amazing job. So I recommend just going with one file for now simply because it's easier to work with.
And later you can test with more files. Now let's go ahead and let's implement the simple delete method. I mean the delete dialog. So what I'm going to do is I'm going to go back inside of my components and I will create the delete file dialog.tsx. Let's mark this as use client and now let's import thing by thing.
So use mutation from convex react, use state, then let's add the button, then of course let's add all the components needed to develop the dialog that's dialog content description footer header and title then let's go ahead and let's import the API from workspace backend underscore generated API and let's do the same thing but for importing the public file from workspace backend private files. Now let's go ahead and let's create the interface delete file dialog props which are going to be quite similar as to our upload file dialog and it's also going to have file which is a type of public file or null as well as on deleted which is an optional callback. Now let's go ahead and actually open the export const delete file dialog and let's go ahead and assign the props here which means that now we can go ahead and structure all of them. Open on open change file and on deleted. The first thing I'm going to do is I'm going to add the delete file mutation using API files, my apologies, private files, delete file.
Let's also create is deleting, set is deleting, Use mutation, my apologies, use state, false by default. Then let's develop handle delete method which is going to be an asynchronous method. If there is no file attached let's simply return. Otherwise set is deleting the true and then open a try, catch and finally. In the finally set is deleting back to false.
In the catch let's catch the error and let's log it. In the try let's do await delete file. Entry ID file.id. Let's call the onDeleted optional callback and onOpenChange will be false. As simple as that.
Now let's go ahead and let's develop the dialog composition. So we're going to render the dialog. OnOpenChange will be OnOpenChangeProp, OpenProp will be OpenProp, DialogContent will have a class name of SmallMaximumWithMedium DialogueHeader DialogueTitle DeleteFile Let's do DialogueDescription and inside something descriptive as to what we are doing. Are you sure you want to delete this file? This action cannot be undone.
Outside of dialog header, Let's conditionally render if we have a file, a little div here that will help us display what we are deleting So use py4 here so it looks better and has more space Add a new div inside give it a class name rounded large border background muted with 50 opacity and padding 4 inside add a paragraph containing the file name that we are about to delete and give it font medium Below that go ahead and add the type file.type to uppercase and then let's go ahead and add a pipe Size and then render File.Size like this and then finally, oops, let's also give this paragraph a class name here TextMutedForeground and TextSmall Outside of this file conditional let's render the dialog footer. Let's render the button inside. The first one will be to cancel. It's going to be disabled if it's deleting. On click it's simply going to call on open change and set false.
The variant will be outline. Let's go ahead and copy this button and let's call this one delete. In fact we can just do the same thing. Is deleting, deleting, otherwise delete. And let's give it a variant of destructive.
Let's disable it if we are deleting or if there is no file selected. And let's call handle delete on click. Perfect. That's it. Now let's go back inside of the files view and inside of the files view let's render the delete file dialog.
Let's go ahead and duplicate this and change the upload dialog open to be delete dialog open. And then we can duplicate these and we can add them here and we can change them respectfully. There we go. But what's missing here is the file. So how do we select the file?
Well Let's go ahead and develop one more thing here. Const selectedFile, setSelectedFile, useState and by default it's going to be null. Let's go ahead and change this to be either public file or null. Just ensure that you have public file type imported. And then let's develop const handle delete click here and the prop will be file, public file.
Set selected file, add the file and set upload dialog, my apologies, set delete dialog open will be set to true. SetSelectedFile, add the file and setUploadDialog, my apologies, setDeleteDialogOpen will be set to true. And let's do const handleFileDeleted is very simply going to be setSelectedFile null. And now we can add both of them to the delete dialog here. So that will be the file, selected file and on deleted handleFileDeleted.
And I think I'm, yeah, I'm not using handle delete click yet. So We're going to use handle delete click in the table. Let's go down here and let's find our actions. So our drop down menu. Inside of here we have the delete with an empty arrow function.
Let's change that and let's pass in handle delete click and file inside. So which file? Well, this one, the one we are iterating over right here, and it's a type of public file. So all of these should match properly with the types. So let's double check.
I now have a fully uploaded and embedded file. So I have some content, I seem to be having some chunks, entries, namespaces. These things inside of my rack components are full. And if I go inside of my files here, specifically you have to change this to app, you have this file. Now let's go ahead and let's delete it.
Now let me confirm. This seems to be working because the list was immediately refreshed. In here, inside of my app, file storage, no files have been found. Now let's check my rag here and you can see my chunks are empty, my content is empty, my entries, except my namespaces. So only the namespace has left, simply because the namespace can happen again, organization, right.
But you can see that the entries, content and the chunks are all gone. Our delete method works well. Amazing, amazing job. In the next chapter we are finally ready to implement the search tool which will be able to use these embeddings and generate answers to the user. So just ensure that you are able to add files, they should be able to appear inside of your app file storage.
So always make sure you're looking at the app here when you are in the files. And when you go inside your data, rag, you should be able to see some chunks, some content and some entries. And when you delete file, all of that should go away. Amazing, amazing job. So we double check the dimension number, we created file as a list function and we created amazing UI.
Let's go ahead and merge this. 21 knowledge base. So I'm going to close this. Let's stage all of these changes. 21 knowledge base.
Let's commit and let's go ahead and open a new branch 21 knowledge base and let's publish this branch and as always let's go ahead and let's review this pull request to confirm that we don't have any serious security issues here. And here we have the summary by CodeRabbit. We introduced a comprehensive file management interface with paginated browsing, infinite scroll, and actions for uploading and deleting files. We added dialogues for uploading new files and confirming file deletions, supporting file metadata and user feedback during operations. We implemented a drag-and-drop file upload component with support for file type and size restrictions, as well as visual feedback and upload states.
We improved error handling and user feedback during file upload and deletion process. We updated dependencies to include support for drag and drop file uploads. That's exactly what this chapter was about. And we have just a few comments here. So again, I completely forgot.
I promise next chapter, we're adding toaster. I forgot again. Absolutely. Thank you, CodeRabbit. So in here, private files.ts, it recommends trying to filter the category at the retrieval level, right?
So in here, we are using the actual dot filter. So CodeRabbit is concerned because that's not obviously the most optimized way. So it's look so he wants me to look into if it's possible to do this instead of rag dot list. I will look into this just at the top of my head I'm not sure if we can do that but yes if we can do that it would be the preferred way. Again very very good comment.
Now in here convert entry to public file So it suggests having early mechanisms to detect if entry metadata is incorrect. So if a storage ID is missing or file name is missing, we can't exactly convert that to the public file. So obviously that would be considered invalid metadata. I will look into this. I'm okay with the method that we've made actually.
So yeah, I'm trying to think if this gives us any benefit. Since it is a backend function, having any console warn will be logged. So that will also be sent to sentry so it could be useful but I'm not too sure because I think we have other mechanisms which guarantee these but yeah maybe it wouldn't hurt to just add some more console logs. We'll see. And either way, very good pull request, not any security issues.
That's great. So we merge this pull request. Now let's go back instead of main, let's synchronize the changes and as always when we synchronize the changes, let's go ahead and review our graph. 21 knowledge base merged. Perfect.
And just one more thing I want to left you with before I go. So when it comes to uploading files instead of convex, you have a couple of options. So what we're doing right now is we are uploading it with a simple mutation. Now, The trick with this is that I don't think there's a file size limit, but I'm pretty sure there is a timeout limit. So if you try to upload a huge file, it will probably keep you in this weird upload state for I haven't managed to get an error.
I uploaded a pretty large file when I tested this. It just took a long time and it kind of hanged there but eventually embeddings were created and it all worked great. So just a quick tip for you If you're trying to upload some large files, please, you know, I'd rather you test it with the files I provide you with. Like these are all very, very small text files and they're super easy to test. And then later try PDFs and try images and things like that.
Just a small tip for that. I would recommend taking a look at the upload here so you can file all the different methods of how you can do that. Great, amazing, amazing job and see you in the next chapter.