components/file-upload.tsx (256 lines of code) (raw):
"use client";
import React, { useCallback, useState, FormEvent } from "react";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "./ui/button";
import { FilePlus2, Plus, Trash2, CircleX } from "lucide-react";
import { useDropzone } from "react-dropzone";
import { Input } from "./ui/input";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./ui/tooltip";
interface FileUploadProps {
vectorStoreId?: string;
vectorStoreName?: string;
onAddStore: (id: string) => void;
onUnlinkStore: () => void;
}
export default function FileUpload({
vectorStoreId,
onAddStore,
onUnlinkStore,
}: FileUploadProps) {
const [file, setFile] = useState<File | null>(null);
const [newStoreName, setNewStoreName] = useState<string>("Default store");
const [uploading, setUploading] = useState<boolean>(false);
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const acceptedFileTypes = {
"text/x-c": [".c"],
"text/x-c++": [".cpp"],
"text/x-csharp": [".cs"],
"text/css": [".css"],
"application/msword": [".doc"],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": [
".docx",
],
"text/x-golang": [".go"],
"text/html": [".html"],
"text/x-java": [".java"],
"text/javascript": [".js"],
"application/json": [".json"],
"text/markdown": [".md"],
"application/pdf": [".pdf"],
"text/x-php": [".php"],
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
[".pptx"],
"text/x-python": [".py"],
"text/x-script.python": [".py"],
"text/x-ruby": [".rb"],
"application/x-sh": [".sh"],
"text/x-tex": [".tex"],
"application/typescript": [".ts"],
"text/plain": [".txt"],
};
const onDrop = useCallback((acceptedFiles: File[]) => {
if (acceptedFiles.length > 0) {
setFile(acceptedFiles[0]);
}
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
multiple: false,
accept: acceptedFileTypes,
});
const removeFile = () => {
setFile(null);
};
const arrayBufferToBase64 = (buffer: ArrayBuffer) => {
const bytes = new Uint8Array(buffer);
let binary = "";
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!file) {
alert("Please select a file to upload.");
return;
}
setUploading(true);
try {
const arrayBuffer = await file.arrayBuffer();
const base64Content = arrayBufferToBase64(arrayBuffer);
const fileObject = {
name: file.name,
content: base64Content,
};
// 1. Upload file
const uploadResponse = await fetch("/api/vector_stores/upload_file", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
fileObject,
}),
});
if (!uploadResponse.ok) {
throw new Error("Error uploading file");
}
const uploadData = await uploadResponse.json();
const fileId = uploadData.id;
if (!fileId) {
throw new Error("Error getting file ID");
}
console.log("Uploaded file:", uploadData);
let finalVectorStoreId = vectorStoreId;
// 2. If no vector store is linked, create one
if (!vectorStoreId || vectorStoreId === "") {
const createResponse = await fetch("/api/vector_stores/create_store", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
storeName: newStoreName,
}),
});
if (!createResponse.ok) {
throw new Error("Error creating vector store");
}
const createData = await createResponse.json();
finalVectorStoreId = createData.id;
}
if (!finalVectorStoreId) {
throw new Error("Error getting vector store ID");
}
onAddStore(finalVectorStoreId);
// 3. Add file to vector store
const addFileResponse = await fetch("/api/vector_stores/add_file", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
fileId,
vectorStoreId: finalVectorStoreId,
}),
});
if (!addFileResponse.ok) {
throw new Error("Error adding file to vector store");
}
const addFileData = await addFileResponse.json();
console.log("Added file to vector store:", addFileData);
setFile(null);
setDialogOpen(false);
} catch (error) {
console.error("Error during file upload process:", error);
alert("There was an error processing your file. Please try again.");
} finally {
setUploading(false);
}
};
return (
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<div className="bg-white rounded-full flex items-center justify-center py-1 px-3 border border-zinc-200 gap-1 font-medium text-sm cursor-pointer hover:bg-zinc-50 transition-all">
<Plus size={16} />
Upload
</div>
</DialogTrigger>
<DialogContent className="sm:max-w-[500px] md:max-w-[600px] max-h-[80vh] overflow-y-scrollfrtdtd">
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>Add files to your vector store</DialogTitle>
</DialogHeader>
<div className="my-6">
{!vectorStoreId || vectorStoreId === "" ? (
<div className="flex items-start gap-2 text-sm">
<label className="font-medium w-72" htmlFor="storeName">
New vector store name
<div className="text-xs text-zinc-400">
A new store will be created when you upload a file.
</div>
</label>
<Input
id="storeName"
type="text"
value={newStoreName}
onChange={(e) => setNewStoreName(e.target.value)}
className="border rounded p-2"
/>
</div>
) : (
<div className="flex items-center justify-between flex-1 min-w-0">
<div className="flex items-center gap-2 min-w-0">
<div className="text-sm font-medium w-24 text-nowrap">
Vector store
</div>
<div className="text-zinc-400 text-xs font-mono flex-1 text-ellipsis truncate">
{vectorStoreId}
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<CircleX
onClick={() => onUnlinkStore()}
size={16}
className="cursor-pointer text-zinc-400 mb-0.5 shrink-0 mt-0.5 hover:text-zinc-700 transition-all"
/>
</TooltipTrigger>
<TooltipContent>
<p>Unlink vector store</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
)}
</div>
<div className="flex justify-center items-center mb-4 h-[200px]">
{file ? (
<div className="flex flex-col items-start">
<div className="text-zinc-400">Loaded file</div>
<div className="flex items-center mt-2">
<div className="text-zinc-900 mr-2">{file.name}</div>
<Trash2
onClick={removeFile}
size={16}
className="cursor-pointer text-zinc-900"
/>
</div>
</div>
) : (
<div className="flex flex-col items-center">
<div
{...getRootProps()}
className="p-6 flex items-center justify-center relative focus-visible:outline-0"
>
<input {...getInputProps()} />
<div
className={`absolute rounded-full transition-all duration-300 ${
isDragActive
? "h-56 w-56 bg-zinc-100"
: "h-0 w-0 bg-transparent"
}`}
></div>
<div className="flex flex-col items-center text-center z-10 cursor-pointer">
<FilePlus2 className="mb-4 size-8 text-zinc-700" />
<div className="text-zinc-700">Upload a file</div>
</div>
</div>
</div>
)}
</div>
<DialogFooter>
<Button type="submit" disabled={uploading}>
{uploading ? "Uploading..." : "Add"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}