mirror of
https://github.com/StanGirard/quivr.git
synced 2024-12-24 03:41:56 +03:00
Feat/improve UI (#174)
* feat(signup): add sign in page link * feat(upload): improve ui * ui(header): add logout button * feat(login): add redirection for logged user
This commit is contained in:
parent
1a11f6045e
commit
85f89b4df1
@ -1,16 +1,16 @@
|
||||
"use client";
|
||||
import { useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSupabase } from "@/app/supabase-provider";
|
||||
import Button from "@/app/components/ui/Button";
|
||||
import Card from "@/app/components/ui/Card";
|
||||
import Field from "@/app/components/ui/Field";
|
||||
import PageHeading from "@/app/components/ui/PageHeading";
|
||||
import Toast, { ToastRef } from "@/app/components/ui/Toast";
|
||||
import { useSupabase } from "@/app/supabase-provider";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
export default function Login() {
|
||||
const { supabase } = useSupabase();
|
||||
const { supabase, session } = useSupabase();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
@ -27,22 +27,23 @@ export default function Login() {
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Error logging in:", error.message);
|
||||
loginToast.current?.publish({
|
||||
variant: "danger",
|
||||
text: error.message,
|
||||
});
|
||||
} else if (data) {
|
||||
console.log("User logged in:", data);
|
||||
loginToast.current?.publish({
|
||||
variant: "success",
|
||||
text: "Successfully logged in",
|
||||
});
|
||||
router.replace("/");
|
||||
}
|
||||
setIsPending(false);
|
||||
};
|
||||
|
||||
if (session?.user !== undefined) {
|
||||
router.replace("/");
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<section className="w-full min-h-screen h-full outline-none flex flex-col gap-5 items-center justify-center p-6">
|
||||
|
@ -5,6 +5,7 @@ import Field from "@/app/components/ui/Field";
|
||||
import PageHeading from "@/app/components/ui/PageHeading";
|
||||
import Toast, { ToastRef } from "@/app/components/ui/Toast";
|
||||
import { useSupabase } from "@/app/supabase-provider";
|
||||
import Link from "next/link";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
export default function SignUp() {
|
||||
@ -64,6 +65,7 @@ export default function SignUp() {
|
||||
/>
|
||||
<div className="flex flex-col items-center justify-center mt-2 gap-2">
|
||||
<Button isLoading={isPending}>Sign Up</Button>
|
||||
<Link href="/login">Already registered? Sign in</Link>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
|
@ -1,14 +1,17 @@
|
||||
import { useSupabase } from "@/app/supabase-provider";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import { Dispatch, FC, HTMLAttributes, ReactNode, SetStateAction } from "react";
|
||||
import DarkModeToggle from "./DarkModeToggle";
|
||||
import Button from "../ui/Button";
|
||||
import DarkModeToggle from "./DarkModeToggle";
|
||||
|
||||
interface NavItemsProps extends HTMLAttributes<HTMLUListElement> {
|
||||
setOpen?: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const NavItems: FC<NavItemsProps> = ({ className, setOpen, ...props }) => {
|
||||
const { session } = useSupabase();
|
||||
const isUserLoggedIn = session?.user !== undefined;
|
||||
return (
|
||||
<ul
|
||||
className={cn(
|
||||
@ -44,6 +47,11 @@ const NavItems: FC<NavItemsProps> = ({ className, setOpen, ...props }) => {
|
||||
<Button variant={"secondary"}>Try Demo</Button>
|
||||
</Link>
|
||||
<DarkModeToggle />
|
||||
{isUserLoggedIn && (
|
||||
<Link href={"/logout"}>
|
||||
<Button variant={"secondary"}>Logout</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</ul>
|
||||
);
|
||||
|
27
frontend/app/components/ui/Divider.tsx
Normal file
27
frontend/app/components/ui/Divider.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { FC, HTMLAttributes, LegacyRef, forwardRef } from "react";
|
||||
|
||||
type DividerProps = HTMLAttributes<HTMLDivElement> & {
|
||||
text?: string;
|
||||
};
|
||||
|
||||
const Divider: FC<DividerProps> = forwardRef(
|
||||
({ className, text, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref as LegacyRef<HTMLDivElement>}
|
||||
className={cn("flex items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<hr className="border-t border-gray-300 w-12" />
|
||||
{text !== undefined && (
|
||||
<p className="px-3 bg-white text-center text-gray-500">{text}</p>
|
||||
)}
|
||||
<hr className="border-t border-gray-300 w-12" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
Divider.displayName = "AnimatedCard";
|
||||
|
||||
export { Divider };
|
@ -1,11 +1,11 @@
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
import NavBar from "./components/NavBar";
|
||||
import "./globals.css";
|
||||
import { Inter } from "next/font/google";
|
||||
import SupabaseProvider from "./supabase-provider";
|
||||
import { createServerComponentSupabaseClient } from "@supabase/auth-helpers-nextjs";
|
||||
import { headers, cookies } from "next/headers";
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
import { Inter } from "next/font/google";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import NavBar from "./components/NavBar";
|
||||
import { ToastProvider } from "./components/ui/Toast";
|
||||
import "./globals.css";
|
||||
import SupabaseProvider from "./supabase-provider";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
@ -34,9 +34,11 @@ export default async function RootLayout({
|
||||
<body
|
||||
className={`bg-white text-black dark:bg-black dark:text-white min-h-screen w-full ${inter.className}`}
|
||||
>
|
||||
<NavBar />
|
||||
<ToastProvider>
|
||||
<SupabaseProvider session={session}>{children}</SupabaseProvider>
|
||||
<SupabaseProvider session={session}>
|
||||
<NavBar />
|
||||
{children}
|
||||
</SupabaseProvider>
|
||||
</ToastProvider>
|
||||
<Analytics />
|
||||
</body>
|
||||
|
@ -0,0 +1,8 @@
|
||||
export const isValidUrl = (string: string) => {
|
||||
try {
|
||||
new URL(string);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
};
|
72
frontend/app/upload/components/Crawler/hooks/useCrawler.ts
Normal file
72
frontend/app/upload/components/Crawler/hooks/useCrawler.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { useSupabase } from "@/app/supabase-provider";
|
||||
import axios from "axios";
|
||||
import { redirect } from "next/navigation";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useToast } from "../../../hooks/useToast";
|
||||
import { isValidUrl } from "../helpers/isValidUrl";
|
||||
|
||||
export const useCrawler = () => {
|
||||
const [isCrawling, setCrawling] = useState(false);
|
||||
const urlInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const { setMessage, messageToast } = useToast();
|
||||
const { session } = useSupabase();
|
||||
if (session === null) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
const crawlWebsite = useCallback(async () => {
|
||||
setCrawling(true);
|
||||
// Validate URL
|
||||
const url = urlInputRef.current ? urlInputRef.current.value : null;
|
||||
|
||||
if (!url || !isValidUrl(url)) {
|
||||
// Assuming you have a function to validate URLs
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: "Invalid URL",
|
||||
});
|
||||
setCrawling(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure parameters
|
||||
const config = {
|
||||
url: url,
|
||||
js: false,
|
||||
depth: 1,
|
||||
max_pages: 100,
|
||||
max_time: 60,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${process.env.NEXT_PUBLIC_BACKEND_URL}/crawl`,
|
||||
config,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setMessage({
|
||||
type: response.data.type,
|
||||
text: response.data.message,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: "Failed to crawl website: " + JSON.stringify(error),
|
||||
});
|
||||
} finally {
|
||||
setCrawling(false);
|
||||
}
|
||||
}, [session.access_token]);
|
||||
|
||||
return {
|
||||
isCrawling,
|
||||
urlInputRef,
|
||||
messageToast,
|
||||
crawlWebsite,
|
||||
};
|
||||
};
|
37
frontend/app/upload/components/Crawler/index.tsx
Normal file
37
frontend/app/upload/components/Crawler/index.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
import Button from "@/app/components/ui/Button";
|
||||
import Card from "@/app/components/ui/Card";
|
||||
import Field from "@/app/components/ui/Field";
|
||||
import Toast from "@/app/components/ui/Toast";
|
||||
import { useCrawler } from "./hooks/useCrawler";
|
||||
|
||||
export const Crawler = (): JSX.Element => {
|
||||
const { urlInputRef, isCrawling, messageToast, crawlWebsite } = useCrawler();
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex justify-center gap-5">
|
||||
<div className="w-1/2">
|
||||
<div className="flex-column justify-center gap-5">
|
||||
<Card className="h-32 flex justify-center items-center">
|
||||
<div className="text-center mt-2 p-6 max-w-sm w-full flex flex-col gap-5 items-center">
|
||||
<Field
|
||||
name="crawlurl"
|
||||
ref={urlInputRef}
|
||||
type="text"
|
||||
placeholder="Enter a website URL"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center gap-5">
|
||||
<Button isLoading={isCrawling} onClick={crawlWebsite}>
|
||||
Crawl
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<Toast ref={messageToast} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,44 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { MdClose } from "react-icons/md";
|
||||
|
||||
export 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 flex-row gap-1 py-2 dark:bg-black border-b border-black/10 dark:border-white/25 leading-none px-6"
|
||||
>
|
||||
<div className="flex flex-1">
|
||||
<div className="flex flex flex-col">
|
||||
<span className="overflow-ellipsis overflow-hidden whitespace-nowrap">
|
||||
{file.name}
|
||||
</span>
|
||||
<span className="text-xs opacity-50 overflow-hidden text-ellipsis">
|
||||
{(file.size / 1000).toFixed(3)} kb
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-5">
|
||||
<button
|
||||
role="remove file"
|
||||
className="text-xl text-red-500 text-ellipsis"
|
||||
onClick={() =>
|
||||
setFiles((files) =>
|
||||
files.filter((f) => f.name !== file.name || f.size !== file.size)
|
||||
)
|
||||
}
|
||||
>
|
||||
<MdClose />
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
@ -0,0 +1,103 @@
|
||||
import { useSupabase } from "@/app/supabase-provider";
|
||||
import { useToast } from "@/app/upload/hooks/useToast";
|
||||
import axios from "axios";
|
||||
import { redirect } from "next/navigation";
|
||||
import { useCallback, useState } from "react";
|
||||
import { FileRejection, useDropzone } from "react-dropzone";
|
||||
|
||||
export const useFileUploader = () => {
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const { messageToast, setMessage } = useToast();
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [pendingFileIndex, setPendingFileIndex] = useState<number>(0);
|
||||
const { session } = useSupabase();
|
||||
|
||||
if (session === null) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
const upload = useCallback(
|
||||
async (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${process.env.NEXT_PUBLIC_BACKEND_URL}/upload`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setMessage({
|
||||
type: response.data.type,
|
||||
text:
|
||||
(response.data.type === "success"
|
||||
? "File uploaded successfully: "
|
||||
: "") + JSON.stringify(response.data.message),
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: "Failed to upload file: " + JSON.stringify(error),
|
||||
});
|
||||
}
|
||||
},
|
||||
[session.access_token]
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
setFiles((files) => [...files, ...acceptedFiles]);
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
const { getInputProps, getRootProps, isDragActive, open } = useDropzone({
|
||||
onDrop,
|
||||
noClick: true,
|
||||
maxSize: 100000000, // 1 MB
|
||||
});
|
||||
|
||||
return {
|
||||
isPending,
|
||||
getInputProps,
|
||||
getRootProps,
|
||||
isDragActive,
|
||||
open,
|
||||
uploadAllFiles,
|
||||
pendingFileIndex,
|
||||
messageToast,
|
||||
files,
|
||||
setFiles,
|
||||
};
|
||||
};
|
80
frontend/app/upload/components/FileUploader/index.tsx
Normal file
80
frontend/app/upload/components/FileUploader/index.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
import Toast from "@/app/components/ui/Toast";
|
||||
import { AnimatePresence } from "framer-motion";
|
||||
import Button from "../../../components/ui/Button";
|
||||
import Card from "../../../components/ui/Card";
|
||||
import { FileComponent } from "./components/FileComponent";
|
||||
import { useFileUploader } from "./hooks/useFileUploader";
|
||||
|
||||
export const FileUploader = (): JSX.Element => {
|
||||
const {
|
||||
getInputProps,
|
||||
getRootProps,
|
||||
isDragActive,
|
||||
isPending,
|
||||
messageToast,
|
||||
open,
|
||||
pendingFileIndex,
|
||||
uploadAllFiles,
|
||||
files,
|
||||
setFiles,
|
||||
} = useFileUploader();
|
||||
|
||||
return (
|
||||
<>
|
||||
<section
|
||||
{...getRootProps()}
|
||||
className="w-full outline-none flex flex-col gap-5 items-center justify-center p-6"
|
||||
>
|
||||
<div className="w-full">
|
||||
<div className="flex justify-center gap-5">
|
||||
{/* Assign a width of 50% to each card */}
|
||||
<div className="w-1/2">
|
||||
<Card className="h-52 flex justify-center items-center">
|
||||
<input {...getInputProps()} />
|
||||
<div className="text-center mt-2 p-6 max-w-sm w-full flex flex-col gap-5 items-center">
|
||||
{isDragActive ? (
|
||||
<p className="text-blue-600">Drop the files here...</p>
|
||||
) : (
|
||||
<button
|
||||
onClick={open}
|
||||
className="opacity-50 h-full cursor-pointer hover:opacity-100 hover:underline transition-opacity"
|
||||
>
|
||||
Drag and drop files here, or click to browse
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{files.length > 0 && (
|
||||
<div className="w-1/2">
|
||||
<Card className="h-52 py-3 overflow-y-auto">
|
||||
{files.length > 0 ? (
|
||||
<AnimatePresence>
|
||||
{files.map((file) => (
|
||||
<FileComponent
|
||||
key={file.name + file.size}
|
||||
file={file}
|
||||
setFiles={setFiles}
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
) : null}
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center mt-5">
|
||||
<Button isLoading={isPending} onClick={uploadAllFiles}>
|
||||
{isPending
|
||||
? `Uploading ${files[pendingFileIndex].name}`
|
||||
: "Upload"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<Toast ref={messageToast} />
|
||||
</>
|
||||
);
|
||||
};
|
25
frontend/app/upload/hooks/useToast.ts
Normal file
25
frontend/app/upload/hooks/useToast.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { ToastRef } from "@/app/components/ui/Toast";
|
||||
import { Message } from "postcss";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export const useToast = () => {
|
||||
const [message, setMessage] = useState<Message | null>(null);
|
||||
const messageToast = useRef<ToastRef>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!message) return;
|
||||
messageToast.current?.publish({
|
||||
variant:
|
||||
message.type === "error"
|
||||
? "danger"
|
||||
: message.type === "warning"
|
||||
? "neutral"
|
||||
: "success",
|
||||
text: message.text,
|
||||
});
|
||||
}, [message]);
|
||||
return {
|
||||
setMessage,
|
||||
messageToast,
|
||||
};
|
||||
};
|
@ -1,280 +1,28 @@
|
||||
"use client";
|
||||
import { Message } from "@/lib/types";
|
||||
import axios from "axios";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { FileRejection, useDropzone } from "react-dropzone";
|
||||
import { MdClose } from "react-icons/md";
|
||||
import Button from "../components/ui/Button";
|
||||
import Card from "../components/ui/Card";
|
||||
import Field from "../components/ui/Field";
|
||||
import { Divider } from "../components/ui/Divider";
|
||||
import PageHeading from "../components/ui/PageHeading";
|
||||
import Toast, { ToastRef } from "../components/ui/Toast";
|
||||
import { useSupabase } from "../supabase-provider";
|
||||
import { Crawler } from "./components/Crawler";
|
||||
import { FileUploader } from "./components/FileUploader";
|
||||
|
||||
export default function UploadPage() {
|
||||
const [message, setMessage] = useState<Message | null>(null);
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [pendingFileIndex, setPendingFileIndex] = useState<number>(0);
|
||||
const urlInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const { session } = useSupabase();
|
||||
if (session === null) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
const messageToast = useRef<ToastRef>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!message) return;
|
||||
messageToast.current?.publish({
|
||||
variant:
|
||||
message.type === "error"
|
||||
? "danger"
|
||||
: message.type === "warning"
|
||||
? "neutral"
|
||||
: "success",
|
||||
text: message.text,
|
||||
});
|
||||
}, [message]);
|
||||
|
||||
const crawlWebsite = useCallback(async () => {
|
||||
// Validate URL
|
||||
const url = urlInputRef.current ? urlInputRef.current.value : null;
|
||||
if (!url || !isValidUrl(url)) {
|
||||
// Assuming you have a function to validate URLs
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: "Invalid URL",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure parameters
|
||||
const config = {
|
||||
url: url,
|
||||
js: false,
|
||||
depth: 1,
|
||||
max_pages: 100,
|
||||
max_time: 60,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${process.env.NEXT_PUBLIC_BACKEND_URL}/crawl`,
|
||||
config,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setMessage({
|
||||
type: response.data.type,
|
||||
text: response.data.message,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: "Failed to crawl website: " + JSON.stringify(error),
|
||||
});
|
||||
}
|
||||
}, [session.access_token]);
|
||||
|
||||
const upload = useCallback(
|
||||
async (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${process.env.NEXT_PUBLIC_BACKEND_URL}/upload`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setMessage({
|
||||
type: response.data.type,
|
||||
text:
|
||||
(response.data.type === "success"
|
||||
? "File uploaded successfully: "
|
||||
: "") + JSON.stringify(response.data.message),
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: "Failed to upload file: " + JSON.stringify(error),
|
||||
});
|
||||
}
|
||||
},
|
||||
[session.access_token]
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
setFiles((files) => [...files, ...acceptedFiles]);
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
|
||||
onDrop,
|
||||
noClick: true,
|
||||
maxSize: 100000000, // 1 MB
|
||||
});
|
||||
|
||||
return (
|
||||
<main>
|
||||
<section
|
||||
{...getRootProps()}
|
||||
// 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-32 flex flex-col gap-5 items-center justify-center p-6"
|
||||
>
|
||||
<PageHeading
|
||||
title="Upload Knowledge"
|
||||
subtitle="Text, document, spreadsheet, presentation, audio, video, and URLs supported"
|
||||
/>
|
||||
{/* 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">
|
||||
<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) => (
|
||||
<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 files here, or click to browse
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
{/* Assign a width of 50% to each card */}
|
||||
<Card className="w-1/2">
|
||||
<div className="text-center mt-2 p-6 max-w-sm w-full flex flex-col gap-5 items-center">
|
||||
<Field
|
||||
name="crawlurl"
|
||||
ref={urlInputRef}
|
||||
type="text"
|
||||
placeholder="Enter a website URL"
|
||||
/>
|
||||
<button
|
||||
onClick={crawlWebsite}
|
||||
className="opacity-50 cursor-pointer hover:opacity-100 hover:underline transition-opacity"
|
||||
>
|
||||
Crawl
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center gap-5">
|
||||
<Button isLoading={isPending} onClick={uploadAllFiles} className="">
|
||||
{isPending ? `Uploading ${files[pendingFileIndex].name}` : "Upload"}
|
||||
<main className="pt-20">
|
||||
<PageHeading
|
||||
title="Upload Knowledge"
|
||||
subtitle="Text, document, spreadsheet, presentation, audio, video, and URLs supported"
|
||||
/>
|
||||
<FileUploader />
|
||||
<Divider text="or" className="m-5" />
|
||||
<Crawler />
|
||||
<div className="flex flex-col items-center justify-center gap-5 mt-5">
|
||||
<Link href={"/chat"}>
|
||||
<Button variant={"secondary"} className="py-3">
|
||||
Chat
|
||||
</Button>
|
||||
<Link href={"/chat"}>
|
||||
<Button variant={"secondary"} className="py-3">
|
||||
Chat
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
<Toast ref={messageToast} />
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user