mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-12-15 09:32:22 +03:00
Mad/UI improvements in /explore (#127)
* feature: delete file * feature: consume /explore/file_name to view details of an uploaded document * feature: optimistic update when deleting file * feature: Loading state for /explore * style: Exit animation * style: responsive card
This commit is contained in:
parent
9f926c2e2b
commit
e4217fe15f
@ -19,6 +19,8 @@ const ButtonVariants = cva(
|
|||||||
tertiary: "text-black dark:text-white bg-transparent py-2 px-4",
|
tertiary: "text-black dark:text-white bg-transparent py-2 px-4",
|
||||||
secondary:
|
secondary:
|
||||||
"border border-black dark:border-white bg-white dark:bg-black text-black dark:text-white focus:bg-black dark:focus:bg-white hover:bg-black dark:hover:bg-white hover:text-white dark:hover:text-black focus:text-white transition-colors py-2 px-4 shadow-none",
|
"border border-black dark:border-white bg-white dark:bg-black text-black dark:text-white focus:bg-black dark:focus:bg-white hover:bg-black dark:hover:bg-white hover:text-white dark:hover:text-black focus:text-white transition-colors py-2 px-4 shadow-none",
|
||||||
|
danger:
|
||||||
|
"border border-red-500 hover:bg-red-500 hover:text-white transition-colors",
|
||||||
},
|
},
|
||||||
brightness: {
|
brightness: {
|
||||||
dim: "",
|
dim: "",
|
||||||
@ -55,5 +57,5 @@ const Button: FC<ButtonProps> = forwardRef(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
Button.displayName = 'Button';
|
Button.displayName = "Button";
|
||||||
export default Button;
|
export default Button;
|
||||||
|
@ -10,9 +10,16 @@ interface ModalProps {
|
|||||||
desc: string;
|
desc: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
Trigger: ReactNode;
|
Trigger: ReactNode;
|
||||||
|
CloseTrigger?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Modal: FC<ModalProps> = ({ title, desc, children, Trigger }) => {
|
const Modal: FC<ModalProps> = ({
|
||||||
|
title,
|
||||||
|
desc,
|
||||||
|
children,
|
||||||
|
Trigger,
|
||||||
|
CloseTrigger,
|
||||||
|
}) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
<Dialog.Root onOpenChange={setOpen}>
|
<Dialog.Root onOpenChange={setOpen}>
|
||||||
@ -51,9 +58,13 @@ const Modal: FC<ModalProps> = ({ title, desc, children, Trigger }) => {
|
|||||||
{children}
|
{children}
|
||||||
|
|
||||||
<Dialog.Close asChild>
|
<Dialog.Close asChild>
|
||||||
<Button variant={"secondary"} className="self-end">
|
{CloseTrigger ? (
|
||||||
Done
|
CloseTrigger
|
||||||
</Button>
|
) : (
|
||||||
|
<Button variant={"secondary"} className="self-end">
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
|
|
||||||
<Dialog.Close asChild>
|
<Dialog.Close asChild>
|
||||||
|
10
frontend/app/components/ui/Spinner.tsx
Normal file
10
frontend/app/components/ui/Spinner.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { FaSpinner } from "react-icons/fa";
|
||||||
|
|
||||||
|
interface SpinnerProps {}
|
||||||
|
|
||||||
|
const Spinner: FC<SpinnerProps> = ({}) => {
|
||||||
|
return <FaSpinner className="animate-spin m-5" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Spinner;
|
@ -1,34 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { FC } from "react";
|
|
||||||
import { Document } from "./types";
|
|
||||||
import Button from "../components/ui/Button";
|
|
||||||
import Modal from "../components/ui/Modal";
|
|
||||||
import { AnimatedCard } from "../components/ui/Card";
|
|
||||||
|
|
||||||
interface DocumentProps {
|
|
||||||
document: Document;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DocumentItem: FC<DocumentProps> = ({ document }) => {
|
|
||||||
return (
|
|
||||||
<AnimatedCard
|
|
||||||
initial={{ x: -64, opacity: 0 }}
|
|
||||||
animate={{ x: 0, opacity: 1 }}
|
|
||||||
className="flex items-center justify-between w-full p-5 gap-10"
|
|
||||||
>
|
|
||||||
<p className="text-lg leading-tight max-w-sm">{document.name}</p>
|
|
||||||
<Modal
|
|
||||||
title={document.name}
|
|
||||||
desc={""}
|
|
||||||
Trigger={<Button className="">View</Button>}
|
|
||||||
>
|
|
||||||
<div className="bg-white py-10 w-full h-1/2 overflow-auto rounded-lg prose">
|
|
||||||
<pre>{JSON.stringify(document, null, 2)}</pre>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</AnimatedCard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
DocumentItem.displayName = "DocumentItem";
|
|
||||||
export default DocumentItem;
|
|
37
frontend/app/explore/DocumentItem/DocumentData.tsx
Normal file
37
frontend/app/explore/DocumentItem/DocumentData.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { Document } from "../types";
|
||||||
|
|
||||||
|
interface DocumentDataProps {
|
||||||
|
documentName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DocumentData = async ({ documentName }: DocumentDataProps) => {
|
||||||
|
const res = await axios.get(
|
||||||
|
`${process.env.NEXT_PUBLIC_BACKEND_URL}/explore/${documentName}`
|
||||||
|
);
|
||||||
|
const documents = res.data.documents as any[];
|
||||||
|
const doc = documents[0];
|
||||||
|
return (
|
||||||
|
<div className="prose">
|
||||||
|
<p>No. of documents: {documents.length}</p>
|
||||||
|
{/* {documents.map((doc) => (
|
||||||
|
<pre key={doc.name}>{JSON.stringify(doc)}</pre>
|
||||||
|
))} */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{documents[0] &&
|
||||||
|
Object.keys(documents[0]).map((k) => {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 border-b py-2" key={k}>
|
||||||
|
<span className="capitalize font-bold">
|
||||||
|
{k.replaceAll("_", " ")}
|
||||||
|
</span>
|
||||||
|
<span className="">{documents[0][k] || "Not Available"}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DocumentData;
|
84
frontend/app/explore/DocumentItem/index.tsx
Normal file
84
frontend/app/explore/DocumentItem/index.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
"use client";
|
||||||
|
import { Document } from "../types";
|
||||||
|
import Button from "../../components/ui/Button";
|
||||||
|
import Modal from "../../components/ui/Modal";
|
||||||
|
import { AnimatedCard } from "../../components/ui/Card";
|
||||||
|
import { Dispatch, SetStateAction, Suspense, useState } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
import DocumentData from "./DocumentData";
|
||||||
|
import Spinner from "@/app/components/ui/Spinner";
|
||||||
|
|
||||||
|
interface DocumentProps {
|
||||||
|
document: Document;
|
||||||
|
setDocuments: Dispatch<SetStateAction<Document[]>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DocumentItem = ({ document, setDocuments }: DocumentProps) => {
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
|
const deleteDocument = async (name: string) => {
|
||||||
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
console.log(`Deleting Document ${name}`);
|
||||||
|
const response = await axios.delete(
|
||||||
|
`${process.env.NEXT_PUBLIC_BACKEND_URL}/explore/${name}`
|
||||||
|
);
|
||||||
|
setDocuments((docs) => docs.filter((doc) => doc.name !== name)); // Optimistic update
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error deleting ${name}`, error);
|
||||||
|
}
|
||||||
|
setIsDeleting(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatedCard
|
||||||
|
initial={{ x: -64, opacity: 0 }}
|
||||||
|
animate={{ x: 0, opacity: 1 }}
|
||||||
|
exit={{ x: 64, opacity: 0 }}
|
||||||
|
className="flex flex-col sm:flex-row sm:items-center justify-between w-full p-5 gap-5"
|
||||||
|
>
|
||||||
|
<p className="text-lg leading-tight max-w-sm">{document.name}</p>
|
||||||
|
<div className="flex gap-2 self-end">
|
||||||
|
{/* VIEW MODAL */}
|
||||||
|
<Modal
|
||||||
|
title={document.name}
|
||||||
|
desc={""}
|
||||||
|
Trigger={<Button className="">View</Button>}
|
||||||
|
>
|
||||||
|
<Suspense fallback={<Spinner />}>
|
||||||
|
{/* @ts-expect-error */}
|
||||||
|
<DocumentData documentName={document.name} />
|
||||||
|
</Suspense>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* DELETE MODAL */}
|
||||||
|
<Modal
|
||||||
|
title={"Confirm"}
|
||||||
|
desc={`Do you really want to delete?`}
|
||||||
|
Trigger={
|
||||||
|
<Button isLoading={isDeleting} variant={"danger"} className="">
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
CloseTrigger={
|
||||||
|
<Button
|
||||||
|
variant={"danger"}
|
||||||
|
isLoading={isDeleting}
|
||||||
|
onClick={() => {
|
||||||
|
deleteDocument(document.name);
|
||||||
|
}}
|
||||||
|
className="self-end"
|
||||||
|
>
|
||||||
|
Delete forever
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>{document.name}</p>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</AnimatedCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
DocumentItem.displayName = "DocumentItem";
|
||||||
|
export default DocumentItem;
|
@ -5,15 +5,19 @@ import DocumentItem from "./DocumentItem";
|
|||||||
import { Document } from "./types";
|
import { Document } from "./types";
|
||||||
import Button from "../components/ui/Button";
|
import Button from "../components/ui/Button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import Spinner from "../components/ui/Spinner";
|
||||||
|
import { AnimatePresence } from "framer-motion";
|
||||||
|
|
||||||
export default function ExplorePage() {
|
export default function ExplorePage() {
|
||||||
const [documents, setDocuments] = useState<Document[]>([]);
|
const [documents, setDocuments] = useState<Document[]>([]);
|
||||||
|
const [isPending, setIsPending] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDocuments();
|
fetchDocuments();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchDocuments = async () => {
|
const fetchDocuments = async () => {
|
||||||
|
setIsPending(true);
|
||||||
try {
|
try {
|
||||||
console.log(
|
console.log(
|
||||||
`Fetching documents from ${process.env.NEXT_PUBLIC_BACKEND_URL}/explore`
|
`Fetching documents from ${process.env.NEXT_PUBLIC_BACKEND_URL}/explore`
|
||||||
@ -26,28 +30,39 @@ export default function ExplorePage() {
|
|||||||
console.error("Error fetching documents", error);
|
console.error("Error fetching documents", error);
|
||||||
setDocuments([]);
|
setDocuments([]);
|
||||||
}
|
}
|
||||||
|
setIsPending(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pt-20 flex flex-col items-center justify-center p-6">
|
<div className="pt-20 flex flex-col items-center justify-center p-6">
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center my-10">
|
||||||
<h1 className="text-3xl font-bold text-center">Explore Your Brain</h1>
|
<h1 className="text-3xl font-bold text-center">Explore Your Brain</h1>
|
||||||
<h2 className="opacity-50">View what’s in your second brain</h2>
|
<h2 className="opacity-50">View what’s in your second brain</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full max-w-xl flex flex-col gap-5">
|
{isPending ? (
|
||||||
{documents.length !== 0 ? (
|
<Spinner />
|
||||||
documents.map((document, index) => (
|
) : (
|
||||||
<DocumentItem key={index} document={document} />
|
<div className="w-full max-w-xl flex flex-col gap-5">
|
||||||
))
|
{documents.length !== 0 ? (
|
||||||
) : (
|
<AnimatePresence>
|
||||||
<div className="flex flex-col items-center justify-center mt-10 gap-1">
|
{documents.map((document) => (
|
||||||
<p className="text-center">Oh No, Your Brain is empty.</p>
|
<DocumentItem
|
||||||
<Link href="/upload">
|
key={document.name}
|
||||||
<Button>Upload Files</Button>
|
document={document}
|
||||||
</Link>
|
setDocuments={setDocuments}
|
||||||
</div>
|
/>
|
||||||
)}
|
))}
|
||||||
</div>
|
</AnimatePresence>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-center mt-10 gap-1">
|
||||||
|
<p className="text-center">Oh No, Your Brain is empty.</p>
|
||||||
|
<Link href="/upload">
|
||||||
|
<Button>Upload Files</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user