mirror of
https://github.com/StanGirard/quivr.git
synced 2024-11-24 05:55:13 +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",
|
||||
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",
|
||||
danger:
|
||||
"border border-red-500 hover:bg-red-500 hover:text-white transition-colors",
|
||||
},
|
||||
brightness: {
|
||||
dim: "",
|
||||
@ -55,5 +57,5 @@ const Button: FC<ButtonProps> = forwardRef(
|
||||
}
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
Button.displayName = "Button";
|
||||
export default Button;
|
||||
|
@ -10,9 +10,16 @@ interface ModalProps {
|
||||
desc: string;
|
||||
children?: 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);
|
||||
return (
|
||||
<Dialog.Root onOpenChange={setOpen}>
|
||||
@ -51,9 +58,13 @@ const Modal: FC<ModalProps> = ({ title, desc, children, Trigger }) => {
|
||||
{children}
|
||||
|
||||
<Dialog.Close asChild>
|
||||
{CloseTrigger ? (
|
||||
CloseTrigger
|
||||
) : (
|
||||
<Button variant={"secondary"} className="self-end">
|
||||
Done
|
||||
</Button>
|
||||
)}
|
||||
</Dialog.Close>
|
||||
|
||||
<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 Button from "../components/ui/Button";
|
||||
import Link from "next/link";
|
||||
import Spinner from "../components/ui/Spinner";
|
||||
import { AnimatePresence } from "framer-motion";
|
||||
|
||||
export default function ExplorePage() {
|
||||
const [documents, setDocuments] = useState<Document[]>([]);
|
||||
const [isPending, setIsPending] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDocuments();
|
||||
}, []);
|
||||
|
||||
const fetchDocuments = async () => {
|
||||
setIsPending(true);
|
||||
try {
|
||||
console.log(
|
||||
`Fetching documents from ${process.env.NEXT_PUBLIC_BACKEND_URL}/explore`
|
||||
@ -26,19 +30,29 @@ export default function ExplorePage() {
|
||||
console.error("Error fetching documents", error);
|
||||
setDocuments([]);
|
||||
}
|
||||
setIsPending(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
<h2 className="opacity-50">View what’s in your second brain</h2>
|
||||
</div>
|
||||
{isPending ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<div className="w-full max-w-xl flex flex-col gap-5">
|
||||
{documents.length !== 0 ? (
|
||||
documents.map((document, index) => (
|
||||
<DocumentItem key={index} document={document} />
|
||||
))
|
||||
<AnimatePresence>
|
||||
{documents.map((document) => (
|
||||
<DocumentItem
|
||||
key={document.name}
|
||||
document={document}
|
||||
setDocuments={setDocuments}
|
||||
/>
|
||||
))}
|
||||
</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>
|
||||
@ -48,6 +62,7 @@ export default function ExplorePage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user