2023-05-18 14:37:03 +03:00
|
|
|
"use client";
|
2023-05-21 19:50:58 +03:00
|
|
|
import { Dispatch, SetStateAction, useCallback, useState, useRef } from "react";
|
2023-05-21 02:20:55 +03:00
|
|
|
import { FileRejection, useDropzone } from "react-dropzone";
|
2023-05-18 14:37:03 +03:00
|
|
|
import axios from "axios";
|
|
|
|
import { Message } from "@/lib/types";
|
2023-05-21 02:20:55 +03:00
|
|
|
import Button from "../components/ui/Button";
|
|
|
|
import { MdClose } from "react-icons/md";
|
|
|
|
import { AnimatePresence, motion } from "framer-motion";
|
|
|
|
import Link from "next/link";
|
|
|
|
import Card from "../components/ui/Card";
|
2023-05-23 11:06:58 +03:00
|
|
|
import PageHeading from "../components/ui/PageHeading";
|
2023-05-21 19:50:58 +03:00
|
|
|
|
2023-05-18 02:22:13 +03:00
|
|
|
export default function UploadPage() {
|
2023-05-21 02:20:55 +03:00
|
|
|
const [message, setMessage] = useState<Message | null>(null);
|
|
|
|
const [isPending, setIsPending] = useState(false);
|
|
|
|
const [files, setFiles] = useState<File[]>([]);
|
|
|
|
const [pendingFileIndex, setPendingFileIndex] = useState<number>(0);
|
2023-05-21 19:50:58 +03:00
|
|
|
const urlInputRef = useRef<HTMLInputElement | null>(null);
|
|
|
|
|
|
|
|
const crawlWebsite = useCallback(async () => {
|
|
|
|
// Validate URL
|
|
|
|
const url = urlInputRef.current ? urlInputRef.current.value : null;
|
2023-05-23 11:06:58 +03:00
|
|
|
if (!url || !isValidUrl(url)) {
|
|
|
|
// Assuming you have a function to validate URLs
|
2023-05-21 19:50:58 +03:00
|
|
|
setMessage({
|
|
|
|
type: "error",
|
2023-05-23 11:06:58 +03:00
|
|
|
text: "Invalid URL",
|
2023-05-21 19:50:58 +03:00
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Configure parameters
|
|
|
|
const config = {
|
|
|
|
url: url,
|
|
|
|
js: false,
|
|
|
|
depth: 1,
|
|
|
|
max_pages: 100,
|
2023-05-23 11:06:58 +03:00
|
|
|
max_time: 60,
|
2023-05-21 19:50:58 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
try {
|
|
|
|
const response = await axios.post(
|
2023-05-22 10:17:25 +03:00
|
|
|
`${process.env.NEXT_PUBLIC_BACKEND_URL}/crawl`,
|
2023-05-21 19:50:58 +03:00
|
|
|
config
|
|
|
|
);
|
|
|
|
|
|
|
|
setMessage({
|
|
|
|
type: response.data.type,
|
|
|
|
text: response.data.message,
|
|
|
|
});
|
|
|
|
} catch (error: any) {
|
|
|
|
setMessage({
|
|
|
|
type: "error",
|
|
|
|
text: "Failed to crawl website: " + error.toString(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}, []);
|
|
|
|
|
2023-05-21 02:20:55 +03:00
|
|
|
const upload = useCallback(async (file: File) => {
|
|
|
|
const formData = new FormData();
|
|
|
|
formData.append("file", file);
|
|
|
|
try {
|
|
|
|
const response = await axios.post(
|
2023-05-21 09:32:22 +03:00
|
|
|
`${process.env.NEXT_PUBLIC_BACKEND_URL}/upload`,
|
2023-05-21 02:20:55 +03:00
|
|
|
formData
|
|
|
|
);
|
2023-05-18 02:22:13 +03:00
|
|
|
|
2023-05-21 02:20:55 +03:00
|
|
|
setMessage({
|
|
|
|
type: response.data.type,
|
|
|
|
text:
|
|
|
|
(response.data.type === "success"
|
|
|
|
? "File uploaded successfully: "
|
|
|
|
: "") + JSON.stringify(response.data.message),
|
|
|
|
});
|
|
|
|
} catch (error: any) {
|
|
|
|
setMessage({
|
|
|
|
type: "error",
|
|
|
|
text: "Failed to upload file: " + error.toString(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}, []);
|
2023-05-18 02:22:13 +03:00
|
|
|
|
2023-05-23 11:06:58 +03:00
|
|
|
const onDrop = (acceptedFiles: File[], fileRejections: FileRejection[]) => {
|
|
|
|
if (fileRejections.length > 0) {
|
|
|
|
setMessage({ type: "error", text: "File too big." });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
setMessage(null);
|
|
|
|
for (let i = 0; i < acceptedFiles.length; i++) {
|
|
|
|
const file = acceptedFiles[i];
|
|
|
|
const isAlreadyInFiles =
|
|
|
|
files.filter((f) => f.name === file.name && f.size === file.size)
|
|
|
|
.length > 0;
|
|
|
|
if (isAlreadyInFiles) {
|
|
|
|
setMessage({ type: "warning", text: `${file.name} was already added` });
|
|
|
|
acceptedFiles.splice(i, 1);
|
2023-05-21 19:50:58 +03:00
|
|
|
}
|
2023-05-23 11:06:58 +03:00
|
|
|
}
|
|
|
|
setFiles((files) => [...files, ...acceptedFiles]);
|
|
|
|
};
|
2023-05-21 02:20:55 +03:00
|
|
|
|
2023-05-23 11:06:58 +03:00
|
|
|
const uploadAllFiles = async () => {
|
|
|
|
setIsPending(true);
|
|
|
|
setMessage(null);
|
|
|
|
// files.forEach((file) => upload(file));
|
|
|
|
for (const file of files) {
|
|
|
|
await upload(file);
|
|
|
|
setPendingFileIndex((i) => i + 1);
|
|
|
|
}
|
|
|
|
setPendingFileIndex(0);
|
|
|
|
setIsPending(false);
|
|
|
|
};
|
2023-05-21 02:20:55 +03:00
|
|
|
|
2023-05-23 11:06:58 +03:00
|
|
|
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
|
|
|
|
onDrop,
|
|
|
|
noClick: true,
|
|
|
|
maxSize: 100000000, // 1 MB
|
|
|
|
});
|
2023-05-21 02:20:55 +03:00
|
|
|
|
2023-05-23 11:06:58 +03:00
|
|
|
return (
|
|
|
|
<main>
|
2023-05-21 02:20:55 +03:00
|
|
|
<section
|
|
|
|
{...getRootProps()}
|
2023-05-23 11:06:58 +03:00
|
|
|
// className="w-full h-full min-h-screen text-center flex flex-col items-center gap-5 pt-32 outline-none"
|
|
|
|
className="w-full outline-none pt-20 flex flex-col gap-5 items-center justify-center p-6"
|
2023-05-21 02:20:55 +03:00
|
|
|
>
|
2023-05-23 11:06:58 +03:00
|
|
|
<PageHeading
|
|
|
|
title="Add Knowledge"
|
|
|
|
subtitle="Upload files to your second brain"
|
|
|
|
/>
|
|
|
|
{/* Wrap the cards in a flex container */}
|
|
|
|
<div className="flex justify-center gap-5">
|
|
|
|
{/* Assign a width of 50% to each card */}
|
|
|
|
<Card className="w-1/2">
|
2023-05-21 19:50:58 +03:00
|
|
|
<input {...getInputProps()} />
|
|
|
|
<div className="text-center mt-2 p-6 max-w-sm w-full flex flex-col gap-5 items-center">
|
|
|
|
{files.length > 0 ? (
|
|
|
|
<AnimatePresence>
|
|
|
|
{files.map((file, i) => (
|
|
|
|
<FileComponent
|
|
|
|
key={file.name + file.size}
|
|
|
|
file={file}
|
|
|
|
setFiles={setFiles}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</AnimatePresence>
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
{isDragActive ? (
|
|
|
|
<p className="text-blue-600">Drop the files here...</p>
|
|
|
|
) : (
|
|
|
|
<button
|
|
|
|
onClick={open}
|
|
|
|
className="opacity-50 cursor-pointer hover:opacity-100 hover:underline transition-opacity"
|
|
|
|
>
|
|
|
|
Drag and drop some files here, or click to browse files
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</Card>
|
2023-05-23 11:06:58 +03:00
|
|
|
{/* Assign a width of 50% to each card */}
|
|
|
|
<Card className="w-1/2">
|
2023-05-21 19:50:58 +03:00
|
|
|
<div className="text-center mt-2 p-6 max-w-sm w-full flex flex-col gap-5 items-center">
|
|
|
|
<input
|
|
|
|
ref={urlInputRef}
|
|
|
|
type="text"
|
|
|
|
placeholder="Enter a website URL"
|
2023-05-23 18:29:14 +03:00
|
|
|
className="dark:bg-black"
|
2023-05-21 19:50:58 +03:00
|
|
|
/>
|
2023-05-21 02:20:55 +03:00
|
|
|
<button
|
2023-05-21 19:50:58 +03:00
|
|
|
onClick={crawlWebsite}
|
2023-05-21 02:20:55 +03:00
|
|
|
className="opacity-50 cursor-pointer hover:opacity-100 hover:underline transition-opacity"
|
|
|
|
>
|
2023-05-21 19:50:58 +03:00
|
|
|
Crawl Website
|
2023-05-21 02:20:55 +03:00
|
|
|
</button>
|
|
|
|
</div>
|
2023-05-21 19:50:58 +03:00
|
|
|
</Card>
|
2023-05-23 11:06:58 +03:00
|
|
|
</div>
|
|
|
|
<div className="flex flex-col items-center justify-center gap-5">
|
|
|
|
<Button isLoading={isPending} onClick={uploadAllFiles} className="">
|
|
|
|
{isPending ? `Adding - ${files[pendingFileIndex].name}` : "Add"}
|
|
|
|
</Button>
|
|
|
|
<Link href={"/chat"}>
|
|
|
|
<Button variant={"secondary"} className="py-3">
|
|
|
|
Start Chatting with your brain
|
2023-05-21 02:20:55 +03:00
|
|
|
</Button>
|
2023-05-23 11:06:58 +03:00
|
|
|
</Link>
|
2023-05-21 19:50:58 +03:00
|
|
|
</div>
|
2023-05-23 11:06:58 +03:00
|
|
|
</section>
|
|
|
|
{message && (
|
|
|
|
<div
|
|
|
|
className={`fixed bottom-0 inset-x-0 m-4 p-4 max-w-sm mx-auto rounded ${
|
|
|
|
message.type === "success"
|
|
|
|
? "bg-green-500"
|
|
|
|
: message.type === "warning"
|
|
|
|
? "bg-yellow-600"
|
|
|
|
: "bg-red-500"
|
|
|
|
}`}
|
2023-05-21 19:50:58 +03:00
|
|
|
>
|
2023-05-23 11:06:58 +03:00
|
|
|
<p className="text-white">{message.text}</p>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</main>
|
|
|
|
);
|
|
|
|
}
|
2023-05-21 19:50:58 +03:00
|
|
|
|
2023-05-23 11:06:58 +03:00
|
|
|
const FileComponent = ({
|
|
|
|
file,
|
|
|
|
setFiles,
|
|
|
|
}: {
|
|
|
|
file: File;
|
|
|
|
setFiles: Dispatch<SetStateAction<File[]>>;
|
|
|
|
}) => {
|
|
|
|
return (
|
|
|
|
<motion.div
|
|
|
|
initial={{ x: "-10%", opacity: 0 }}
|
|
|
|
animate={{ x: "0%", opacity: 1 }}
|
|
|
|
exit={{ x: "10%", opacity: 0 }}
|
|
|
|
className="flex py-2 dark:bg-black border-b border-black/10 dark:border-white/25 w-full text-left leading-none"
|
|
|
|
>
|
|
|
|
<div className="flex flex-col gap-1 flex-1">
|
|
|
|
<span className="flex-1 mr-10">{file.name}</span>
|
|
|
|
<span className="text-xs opacity-50">
|
|
|
|
{(file.size / 1000).toFixed(3)} kb
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
<button
|
|
|
|
role="remove file"
|
|
|
|
className="ml-5 text-xl text-red-500 px-5"
|
|
|
|
onClick={() =>
|
|
|
|
setFiles((files) =>
|
|
|
|
files.filter((f) => f.name !== file.name || f.size !== file.size)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
>
|
|
|
|
<MdClose />
|
|
|
|
</button>
|
|
|
|
</motion.div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
function isValidUrl(string: string) {
|
|
|
|
try {
|
|
|
|
new URL(string);
|
|
|
|
return true;
|
|
|
|
} catch (_) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|