1
1
mirror of https://github.com/QuivrHQ/quivr.git synced 2024-12-15 01:21:48 +03:00

feat: knowledge tab list ()

*  get all knowledge utils

*  add remove knowledge and update router

*  new knowledge provider

* 🚨 remove eslint -disable

*  new useKnowledgeApi

*  set up KnowledgeItem

*  add KnowledgeTable component in knowledge tab

* 🔥 remove DocumentData replaced by KnowledgeItem

* 🐛 fix weird characters instead of '/'

* 💄 truncate knowledge name

*  add DownloadUploadedKnowledge component

* ⚰️ unused code

* 🏷️ introduce UploadedKnowledge and CrawledKnowledge types

* 💄 remove thread

* 💄 bin for delete knowledge

* 🌐 update wording for knowledge tab

* 🔇 remove logs and comments
This commit is contained in:
Zineb El Bachiri 2023-09-22 16:06:04 +02:00 committed by GitHub
parent 48bdbbb3e9
commit d2b4ef4aff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 706 additions and 426 deletions

View File

@ -108,3 +108,13 @@ class Brain(BaseModel):
file_name_with_brain_id = f"{self.id}/{file_name}"
self.supabase_client.storage.from_("quivr").remove([file_name_with_brain_id])
return self.supabase_db.delete_file_from_brain(self.id, file_name) # type: ignore
def get_all_knowledge_in_brain(self):
"""
Retrieve unique brain data (i.e. uploaded files and crawled websites).
"""
vector_ids = self.supabase_db.get_brain_vector_ids(self.id) # type: ignore
self.files = get_unique_files_from_vector_ids(vector_ids)
return self.files

View File

@ -251,3 +251,7 @@ class Repository(ABC):
@abstractmethod
def get_knowledge_by_id(self, knowledge_id: UUID):
pass
@abstractmethod
def get_all_knowledge_in_brain(self, brain_id: UUID):
pass

View File

@ -1,4 +1,4 @@
from typing import Optional
from typing import List, Optional
from uuid import UUID
from fastapi import HTTPException
@ -36,7 +36,7 @@ class Knowledges(Repository):
return Knowledge(**response[0])
def remove_knowledge_by_id(
# todo: remove brain
# todo: update remove brain endpoints to first delete the knowledge
self,
knowledge_id: UUID,
) -> DeleteKnowledgeResponse:
@ -73,8 +73,23 @@ class Knowledges(Repository):
knowledge = (
self.db.from_("knowledge")
.select("*")
.filter("knowledge_id", "eq", str(knowledge_id))
.filter("id", "eq", str(knowledge_id))
.execute()
).data
return Knowledge(**knowledge[0])
def get_all_knowledge_in_brain(self, brain_id: UUID) -> List[Knowledge]:
"""
Get all the knowledge in a brain
Args:
brain_id (UUID): The id of the brain
"""
all_knowledge = (
self.db.from_("knowledge")
.select("*")
.filter("brain_id", "eq", str(brain_id))
.execute()
).data
return all_knowledge

View File

@ -0,0 +1,17 @@
from multiprocessing import get_logger
from models import get_supabase_client
from supabase.client import Client
logger = get_logger()
def delete_file_from_storage(file_identifier: str):
supabase_client: Client = get_supabase_client()
try:
response = supabase_client.storage.from_("quivr").remove([file_identifier])
return response
except Exception as e:
logger.error(e)
raise e

View File

@ -13,7 +13,12 @@ def generate_file_signed_url(path):
try:
response = supabase_client.storage.from_("quivr").create_signed_url(
path, SIGNED_URL_EXPIRATION_PERIOD_IN_SECONDS
path,
SIGNED_URL_EXPIRATION_PERIOD_IN_SECONDS,
options={
"download": True,
"transform": None,
},
)
logger.info("RESPONSE SIGNED URL", response)
return response

View File

@ -0,0 +1,14 @@
from uuid import UUID
from logger import get_logger
from models.settings import get_supabase_db
logger = get_logger(__name__)
def get_all_knowledge(brain_id: UUID):
supabase_db = get_supabase_db()
knowledges = supabase_db.get_all_knowledge_in_brain(brain_id)
return knowledges

View File

@ -0,0 +1,15 @@
from uuid import UUID
from logger import get_logger
from models.knowledge import Knowledge
from models.settings import get_supabase_db
logger = get_logger(__name__)
def get_knowledge(knowledge_id: UUID) -> Knowledge:
supabase_db = get_supabase_db()
knowledge = supabase_db.get_knowledge_by_id(knowledge_id)
return knowledge

View File

@ -0,0 +1,16 @@
from uuid import UUID
from logger import get_logger
from models.settings import get_supabase_db
logger = get_logger(__name__)
def remove_knowledge(knowledge_id: UUID):
supabase_db = get_supabase_db()
message = supabase_db.remove_knowledge_by_id(knowledge_id)
logger.info(f"Knowledge { knowledge_id} removed successfully from table")
return message

View File

@ -3,9 +3,12 @@ from uuid import UUID
from auth import AuthBearer, get_current_user
from fastapi import APIRouter, Depends, Query
from logger import get_logger
from models import Brain, UserIdentity, get_supabase_db
from models import Brain, UserIdentity
from repository.files.delete_file import delete_file_from_storage
from repository.files.generate_file_signed_url import generate_file_signed_url
from repository.files.list_files import list_files_from_storage
from repository.knowledge.get_all_knowledge import get_all_knowledge
from repository.knowledge.get_knowledge import get_knowledge
from repository.knowledge.remove_knowledge import remove_knowledge
from routes.authorizations.brain_authorization import (
RoleEnum,
has_brain_authorization,
@ -29,22 +32,14 @@ async def list_knowledge_in_brain_endpoint(
validate_brain_authorization(brain_id=brain_id, user_id=current_user.id)
brain = Brain(id=brain_id)
knowledges = get_all_knowledge(brain_id)
logger.info("List of knowledge from knowledge table", knowledges)
files = list_files_from_storage(str(brain_id))
logger.info("List of files from storage", files)
# TO DO: Retrieve from Knowledge table instead of storage or vectors
unique_data = brain.get_unique_brain_files()
print("UNIQUE DATA", unique_data)
unique_data.sort(key=lambda x: int(x["size"]), reverse=True)
return {"documents": unique_data}
return {"knowledges": knowledges}
@knowledge_router.delete(
"/knowledge/{file_name}/",
"/knowledge/{knowledge_id}/",
dependencies=[
Depends(AuthBearer()),
Depends(has_brain_authorization(RoleEnum.Owner)),
@ -52,18 +47,27 @@ async def list_knowledge_in_brain_endpoint(
tags=["Knowledge"],
)
async def delete_endpoint(
file_name: str,
knowledge_id: UUID,
current_user: UserIdentity = Depends(get_current_user),
brain_id: UUID = Query(..., description="The ID of the brain"),
):
"""
Delete a specific user file by file name.
Delete a specific knowledge from a brain.
"""
validate_brain_authorization(brain_id=brain_id, user_id=current_user.id)
brain = Brain(id=brain_id)
brain.delete_file_from_brain(file_name)
knowledge = get_knowledge(knowledge_id)
file_name = knowledge.file_name if knowledge.file_name else knowledge.url
remove_knowledge(knowledge_id)
if knowledge.file_name:
delete_file_from_storage(f"{brain_id}/{knowledge.file_name}")
brain.delete_file_from_brain(knowledge.file_name)
elif knowledge.url:
brain.delete_file_from_brain(knowledge.url)
return {
"message": f"{file_name} of brain {brain_id} has been deleted by user {current_user.email}."
@ -71,40 +75,27 @@ async def delete_endpoint(
@knowledge_router.get(
"/explore/{file_name}/signed_download_url",
"/knowledge/{knowledge_id}/signed_download_url",
dependencies=[Depends(AuthBearer())],
tags=["Knowledge"],
)
async def generate_signed_url_endpoint(
file_name: str, current_user: UserIdentity = Depends(get_current_user)
knowledge_id: UUID,
current_user: UserIdentity = Depends(get_current_user),
):
"""
Generate a signed url to download the file from storage.
"""
# check if user has the right to get the file: add brain_id to the query
supabase_db = get_supabase_db()
response = supabase_db.get_vectors_by_file_name(file_name)
documents = response.data
knowledge = get_knowledge(knowledge_id)
if len(documents) == 0:
return {"documents": []}
validate_brain_authorization(brain_id=knowledge.brain_id, user_id=current_user.id)
related_brain_id = (
documents[0]["brains_vectors"][0]["brain_id"]
if len(documents[0]["brains_vectors"]) != 0
else None
)
if related_brain_id is None:
raise Exception(f"File {file_name} has no brain_id associated with it")
if knowledge.file_name == None:
raise Exception(f"Knowledge {knowledge_id} has no file_name associated with it")
file_path_in_storage = f"{related_brain_id}/{file_name}"
file_path_in_storage = f"{knowledge.brain_id}/{knowledge.file_name}"
print("FILE PATH IN STORAGE", file_path_in_storage)
file_signed_url = generate_file_signed_url(file_path_in_storage)
print("FILE SIGNED URL", file_signed_url)
validate_brain_authorization(brain_id=related_brain_id, user_id=current_user.id)
return file_signed_url

View File

@ -1,93 +0,0 @@
/* eslint-disable */
import { useEffect, useState } from "react";
import { useAxios } from "@/lib/hooks";
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
import { useTranslation } from "react-i18next";
import { useSupabase } from "../../../../../../../../lib/context/SupabaseProvider";
interface DocumentDataProps {
documentName: string;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type DocumentDetails = any;
//TODO: review this component logic, types and purposes
const DocumentData = ({ documentName }: DocumentDataProps): JSX.Element => {
const { session } = useSupabase();
const { axiosInstance } = useAxios();
const { track } = useEventTracking();
const { t } = useTranslation(["translation", "explore"]);
const [documents, setDocuments] = useState<DocumentDetails[]>([]);
const [loading, setLoading] = useState<Boolean>(false);
if (!session) {
throw new Error(t("sessionNotFound", { ns: "explore" }));
}
useEffect(() => {
const fetchDocuments = async () => {
setLoading(true);
void track("GET_DOCUMENT_DETAILS");
try {
const res = await axiosInstance.get<{ documents: DocumentDetails[] }>(
`/explore/${documentName}/`
);
setDocuments(res.data.documents);
} catch (error) {
setDocuments([]);
console.error(error);
}
setLoading(false);
};
fetchDocuments();
}, [axiosInstance, documentName]);
function Data() {
return (
<div className="prose dark:prose-invert">
<h1
data-testid="document-name"
className="text-bold text-3xl break-words"
>
{documentName}
</h1>
{documents.length > 0 ? (
<>
<p>
{t("chunkNumber", { quantity: documents.length, ns: "explore" })}
</p>
<div className="flex flex-col">
{Object.entries(documents[0]).map(([key, value]) => {
if (value && typeof value === "object") return;
return (
<div className="grid grid-cols-2 py-2 border-b" key={key}>
<p className="capitalize font-bold break-words">
{key.replaceAll("_", " ")}
</p>
<span className="break-words my-auto">
{String(value || t("notAvailable", { ns: "explore" }))}
</span>
</div>
);
})}
</div>
</>
) : (
<p>{t("notAvailable", { ns: "explore" })}</p>
)}
</div>
);
}
if (loading) {
return <div>{t("loading")}</div>;
} else {
return <Data />;
}
};
export default DocumentData;

View File

@ -1,127 +0,0 @@
/* eslint-disable max-lines */
"use client";
import {
Dispatch,
forwardRef,
RefObject,
SetStateAction,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import Button from "@/lib/components/ui/Button";
import { AnimatedCard } from "@/lib/components/ui/Card";
import Ellipsis from "@/lib/components/ui/Ellipsis";
import { Modal } from "@/lib/components/ui/Modal";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useAxios, useToast } from "@/lib/hooks";
import { Document } from "@/lib/types/Document";
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
import DocumentData from "./DocumentData";
interface DocumentProps {
document: Document;
setDocuments: Dispatch<SetStateAction<Document[]>>;
}
const DocumentItem = forwardRef(
({ document, setDocuments }: DocumentProps, forwardedRef) => {
const [isDeleting, setIsDeleting] = useState(false);
const { publish } = useToast();
const { session } = useSupabase();
const { axiosInstance } = useAxios();
const { track } = useEventTracking();
const { currentBrain } = useBrainContext();
const canDeleteFile = currentBrain?.role === "Owner";
const { t } = useTranslation(["translation", "explore"]);
if (!session) {
throw new Error(t("sessionNotFound", { ns: "explore" }));
}
const deleteDocument = async (name: string) => {
setIsDeleting(true);
void track("DELETE_DOCUMENT");
try {
if (currentBrain?.id === undefined) {
throw new Error(t("noBrain", { ns: "explore" }));
}
await axiosInstance.delete(
`/explore/${name}/?brain_id=${currentBrain.id}`
);
setDocuments((docs) => docs.filter((doc) => doc.name !== name)); // Optimistic update
publish({
variant: "success",
text: t("deleted", {
fileName: name,
brain: currentBrain.name,
ns: "explore",
}),
});
} catch (error) {
publish({
variant: "warning",
text: t("errorDeleting", { fileName: name, ns: "explore" }),
});
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 }}
layout
ref={forwardedRef as RefObject<HTMLDivElement>}
className="flex flex-col sm:flex-row sm:items-center justify-between w-full p-5 gap-5"
>
<Ellipsis tooltip maxCharacters={30}>
{document.name}
</Ellipsis>
<div className="flex gap-2 self-end">
<Modal
Trigger={
<Button className="">{t("view", { ns: "explore" })}</Button>
}
>
<DocumentData documentName={document.name} />
</Modal>
{canDeleteFile && (
<Modal
title={t("deleteConfirmTitle", { ns: "explore" })}
desc={t("deleteConfirmText", { ns: "explore" })}
Trigger={
<Button isLoading={isDeleting} variant={"danger"} className="">
{t("deleteButton")}
</Button>
}
CloseTrigger={
<Button
variant={"danger"}
isLoading={isDeleting}
onClick={() => {
void deleteDocument(document.name);
}}
className="self-end"
>
{t("deleteForeverButton")}
</Button>
}
>
<p>{document.name}</p>
</Modal>
)}
</div>
</AnimatedCard>
);
}
);
DocumentItem.displayName = "DocumentItem";
export default DocumentItem;

View File

@ -0,0 +1,31 @@
"use client";
import { MdLink } from "react-icons/md";
import { CrawledKnowledge } from "@/lib/types/Knowledge";
import { DeleteKnowledge } from "./DeleteKnowledge";
export const CrawledKnowledgeItem = ({
knowledge,
}: {
knowledge: CrawledKnowledge;
}): JSX.Element => {
return (
<tr key={knowledge.id}>
<td className="px-6 py-4 whitespace-nowrap">
<a href={knowledge.url} target="_blank" rel="noopener noreferrer">
<MdLink size="20" color="gray" />
</a>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
<p className={"max-w-[400px] truncate"}>{knowledge.url}</p>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<DeleteKnowledge knowledge={knowledge} />
</td>
</tr>
);
};

View File

@ -0,0 +1,35 @@
"use client";
import { MdDelete } from "react-icons/md";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { Knowledge } from "@/lib/types/Knowledge";
import { useKnowledgeItem } from "./useKnowledgeItem";
export const DeleteKnowledge = ({
knowledge,
}: {
knowledge: Knowledge;
}): JSX.Element => {
const { isDeleting, onDeleteKnowledge } = useKnowledgeItem();
const { currentBrain } = useBrainContext();
const canDeleteFile = currentBrain?.role === "Owner";
console.log("isDeleting", isDeleting);
return (
<>
{canDeleteFile && (
<button
className="text-red-600 hover:text-red-900"
onClick={() => void onDeleteKnowledge(knowledge)}
>
<MdDelete size="20" />
</button>
)}
</>
);
};

View File

@ -0,0 +1,48 @@
import axios from "axios";
import { BsFillCloudArrowDownFill } from "react-icons/bs";
import { useKnowledgeApi } from "@/lib/api/knowledge/useKnowledgeApi";
import { getFileIcon } from "@/lib/helpers/getFileIcon";
import { UploadedKnowledge } from "@/lib/types/Knowledge";
export const DownloadUploadedKnowledge = ({
knowledge,
}: {
knowledge: UploadedKnowledge;
}): JSX.Element => {
const { generateSignedUrlKnowledge } = useKnowledgeApi();
const downloadFile = async () => {
const download_url = await generateSignedUrlKnowledge({
knowledgeId: knowledge.id,
});
try {
const response = await axios.get(download_url, {
responseType: "blob",
});
const blobUrl = window.URL.createObjectURL(new Blob([response.data]));
const a = document.createElement("a");
a.href = blobUrl;
a.download = knowledge.fileName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(blobUrl);
} catch (error) {
console.error("Error downloading the file:", error);
}
};
return (
<a
onClick={() => void downloadFile()}
style={{ display: "flex", flexDirection: "column", alignItems: "center" }}
>
{getFileIcon(knowledge.fileName)}
<BsFillCloudArrowDownFill fontSize="small" />
</a>
);
};

View File

@ -0,0 +1,28 @@
"use client";
import { UploadedKnowledge } from "@/lib/types/Knowledge";
import { DeleteKnowledge } from "./DeleteKnowledge";
import { DownloadUploadedKnowledge } from "./DownloadUploadedKnowledge";
export const UploadedKnowledgeItem = ({
knowledge,
}: {
knowledge: UploadedKnowledge;
}): JSX.Element => {
return (
<tr key={knowledge.id}>
<td className="px-6 py-4 whitespace-nowrap">
<DownloadUploadedKnowledge knowledge={knowledge} />
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
<p className={"max-w-[400px] truncate"}>{knowledge.fileName}</p>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<DeleteKnowledge knowledge={knowledge} />
</td>
</tr>
);
};

View File

@ -0,0 +1,21 @@
"use client";
import { isUploadedKnowledge, Knowledge } from "@/lib/types/Knowledge";
import { CrawledKnowledgeItem } from "./CrawledKnowledgeItem";
import { UploadedKnowledgeItem } from "./UploadedKnowledgeItem";
const KnowledgeItem = ({
knowledge,
}: {
knowledge: Knowledge;
}): JSX.Element => {
return isUploadedKnowledge(knowledge) ? (
<UploadedKnowledgeItem knowledge={knowledge} />
) : (
<CrawledKnowledgeItem knowledge={knowledge} />
);
};
KnowledgeItem.displayName = "KnowledgeItem";
export default KnowledgeItem;

View File

@ -0,0 +1,59 @@
"use client";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useKnowledgeApi } from "@/lib/api/knowledge/useKnowledgeApi";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useKnowledgeContext } from "@/lib/context/KnowledgeProvider/hooks/useKnowledgeContext";
import { useToast } from "@/lib/hooks";
import { Knowledge } from "@/lib/types/Knowledge";
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useKnowledgeItem = () => {
const { deleteKnowledge } = useKnowledgeApi();
const [isDeleting, setIsDeleting] = useState(false);
const { publish } = useToast();
const { track } = useEventTracking();
const { currentBrain } = useBrainContext();
const { setAllKnowledge } = useKnowledgeContext();
const { t } = useTranslation(["translation", "explore"]);
const onDeleteKnowledge = async (knowledge: Knowledge) => {
setIsDeleting(true);
void track("DELETE_DOCUMENT");
const knowledge_name = knowledge.file_name ?? knowledge.url;
try {
if (currentBrain?.id === undefined) {
throw new Error(t("noBrain", { ns: "explore" }));
}
await deleteKnowledge({
brainId: currentBrain.id,
knowledgeId: knowledge.id,
});
setAllKnowledge((knowledges) =>
knowledges.filter((k) => k.id !== knowledge.id)
); // Optimistic update
publish({
variant: "success",
text: t("deleted", {
fileName: knowledge_name,
brain: currentBrain.name,
ns: "explore",
}),
});
} catch (error) {
publish({
variant: "warning",
text: t("errorDeleting", { fileName: knowledge_name, ns: "explore" }),
});
console.error(`Error deleting ${knowledge_name ?? ""}`, error);
}
setIsDeleting(false);
};
return {
isDeleting,
onDeleteKnowledge,
};
};

View File

@ -0,0 +1,23 @@
import { Knowledge } from "@/lib/types/Knowledge";
import KnowledgeItem from "./KnowledgeItem";
interface KnowledgeTableProps {
knowledgeList: Knowledge[];
}
export const KnowledgeTable = ({
knowledgeList,
}: KnowledgeTableProps): JSX.Element => {
return (
<div className="w-full">
<table className="min-w-full divide-y divide-gray-200">
<tbody className="bg-white divide-y divide-gray-200">
{knowledgeList.map((knowledge) => (
<KnowledgeItem knowledge={knowledge} key={knowledge.id} />
))}
</tbody>
</table>
</div>
);
};

View File

@ -1,34 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { UUID } from "crypto";
import { useEffect, useState } from "react";
import { getBrainKnowledgeDataKey } from "@/lib/api/brain/config";
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
import { Document } from "@/lib/types/Document";
type useKnowledgeTabProps = {
brainId: UUID;
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useKnowledgeTab = ({ brainId }: useKnowledgeTabProps) => {
const [documents, setDocuments] = useState<Document[]>([]);
const { getBrainDocuments } = useBrainApi();
const { data: brainDocuments, isLoading: isPending } = useQuery({
queryKey: [getBrainKnowledgeDataKey(brainId)],
queryFn: () => getBrainDocuments(brainId),
});
useEffect(() => {
if (brainDocuments === undefined) {
return;
}
setDocuments(brainDocuments);
}, [brainDocuments]);
return {
isPending,
documents,
setDocuments,
};
};

View File

@ -0,0 +1,34 @@
import { useQuery } from "@tanstack/react-query";
import { UUID } from "crypto";
import { useEffect, useState } from "react";
import { getKnowledgeDataKey } from "@/lib/api/knowledge/config";
import { useKnowledgeApi } from "@/lib/api/knowledge/useKnowledgeApi";
import { Knowledge } from "@/lib/types/Knowledge";
type useKnowledgeTabProps = {
brainId: UUID;
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useKnowledgeTab = ({ brainId }: useKnowledgeTabProps) => {
const [allKnowledge, setAllKnowledge] = useState<Knowledge[]>([]);
const { getAllKnowledge } = useKnowledgeApi();
const { data: brainKnowledges, isLoading: isPending } = useQuery({
queryKey: [getKnowledgeDataKey(brainId)],
queryFn: () => getAllKnowledge({ brainId }),
});
useEffect(() => {
if (brainKnowledges === undefined) {
return;
}
setAllKnowledge(brainKnowledges);
}, [brainKnowledges]);
return {
isPending,
allKnowledge,
setAllKnowledge,
};
};

View File

@ -4,53 +4,50 @@ import { AnimatePresence, motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import Spinner from "@/lib/components/ui/Spinner";
import { KnowledgeProvider } from "@/lib/context";
import DocumentItem from "./DocumentItem";
import { useKnowledgeTab } from "./hooks/useFileManagementTab";
import { KnowledgeTable } from "./KnowledgeTable";
import { useKnowledgeTab } from "./hooks/useKnowledgeTab";
type KnowledgeTabProps = {
brainId: UUID;
};
export const KnowledgeTab = ({ brainId }: KnowledgeTabProps): JSX.Element => {
const { t } = useTranslation(["translation", "explore"]);
const { documents, setDocuments, isPending } = useKnowledgeTab({
const { isPending, allKnowledge } = useKnowledgeTab({
brainId,
});
return (
<main>
<section className="w-full outline-none pt-10 flex flex-col gap-5 items-center justify-center p-6">
<div className="flex flex-col items-center justify-center">
<h1 className="text-3xl font-bold text-center">
{t("title", { ns: "explore" })}
</h1>
<h2 className="opacity-50">{t("subtitle", { ns: "explore" })}</h2>
</div>
{isPending ? (
<Spinner />
) : (
<motion.div layout className="w-full max-w-xl flex flex-col gap-5">
{documents.length !== 0 ? (
<AnimatePresence mode="popLayout">
{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">{t("empty", { ns: "explore" })}</p>
<p className="text-center">
{t("feed_brain_instructions", { ns: "explore" })}
</p>
</div>
)}
</motion.div>
)}
</section>
</main>
<KnowledgeProvider>
<main>
<section className="w-full outline-none pt-10 flex flex-col gap-5 items-center justify-center p-6">
<div className="flex flex-col items-center justify-center">
<h1 className="text-3xl font-bold text-center">
{t("title", { ns: "explore" })}
</h1>
<h2 className="opacity-50">{t("subtitle", { ns: "explore" })}</h2>
</div>
{isPending ? (
<Spinner />
) : (
<motion.div layout className="w-full max-w-xl flex flex-col gap-5">
{allKnowledge.length !== 0 ? (
<AnimatePresence mode="popLayout">
<KnowledgeTable knowledgeList={allKnowledge} />
</AnimatePresence>
) : (
<div className="flex flex-col items-center justify-center mt-10 gap-1">
<p className="text-center">{t("empty", { ns: "explore" })}</p>
<p className="text-center">
{t("feed_brain_instructions", { ns: "explore" })}
</p>
</div>
)}
</motion.div>
)}
</section>
</main>
</KnowledgeProvider>
);
};

View File

@ -1,6 +1,7 @@
"use client";
import { ReactNode } from "react";
import { KnowledgeProvider } from "@/lib/context";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { redirectToLogin } from "@/lib/router/redirectToLogin";
@ -19,7 +20,7 @@ const Layout = ({ children }: LayoutProps): JSX.Element => {
return (
<div className="relative h-full w-full flex justify-stretch items-stretch">
<BrainsList />
{children}
<KnowledgeProvider>{children}</KnowledgeProvider>
</div>
);
};

View File

@ -0,0 +1,6 @@
import { UUID } from "crypto";
const brainDataKey = "quivr-knowledge";
export const getKnowledgeDataKey = (knowledgeId: UUID): string =>
`${brainDataKey}-${knowledgeId}`;

View File

@ -0,0 +1,68 @@
import { AxiosInstance } from "axios";
import { UUID } from "crypto";
import { Knowledge } from "@/lib/types/Knowledge";
export type GetAllKnowledgeInputProps = {
brainId: UUID;
};
interface BEKnowledge {
id: UUID;
brain_id: UUID;
file_name: string | null;
url: string | null;
extension: string;
}
export const getAllKnowledge = async (
{ brainId }: GetAllKnowledgeInputProps,
axiosInstance: AxiosInstance
): Promise<Knowledge[]> => {
const response = await axiosInstance.get<{
knowledges: BEKnowledge[];
}>(`/knowledge?brain_id=${brainId}`);
return response.data.knowledges.map((knowledge) => {
if (knowledge.file_name !== null) {
return {
id: knowledge.id,
brainId: knowledge.brain_id,
fileName: knowledge.file_name,
extension: knowledge.extension,
};
} else if (knowledge.url !== null) {
return {
id: knowledge.id,
brainId: knowledge.brain_id,
url: knowledge.url,
extension: "URL",
};
} else {
throw new Error(`Invalid knowledge ${knowledge.id}`);
}
});
};
export type DeleteKnowledgeInputProps = {
brainId: UUID;
knowledgeId: UUID;
};
export const deleteKnowledge = async (
{ knowledgeId, brainId }: DeleteKnowledgeInputProps,
axiosInstance: AxiosInstance
): Promise<void> => {
await axiosInstance.delete(`/knowledge/${knowledgeId}?brain_id=${brainId}`);
};
export const generateSignedUrlKnowledge = async (
{ knowledgeId }: { knowledgeId: UUID },
axiosInstance: AxiosInstance
): Promise<string> => {
const response = await axiosInstance.get<{
signedURL: string;
}>(`/knowledge/${knowledgeId}/signed_download_url`);
return response.data.signedURL;
};

View File

@ -0,0 +1,25 @@
import { UUID } from "crypto";
import { useAxios } from "@/lib/hooks";
import {
deleteKnowledge,
DeleteKnowledgeInputProps,
generateSignedUrlKnowledge,
getAllKnowledge,
GetAllKnowledgeInputProps,
} from "./knowledge";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useKnowledgeApi = () => {
const { axiosInstance } = useAxios();
return {
getAllKnowledge: async (props: GetAllKnowledgeInputProps) =>
getAllKnowledge(props, axiosInstance),
deleteKnowledge: async (props: DeleteKnowledgeInputProps) =>
deleteKnowledge(props, axiosInstance),
generateSignedUrlKnowledge: async (props: { knowledgeId: UUID }) =>
generateSignedUrlKnowledge(props, axiosInstance),
};
};

View File

@ -14,4 +14,7 @@ i18n.use(initReactI18next).init({
defaultNS,
resources,
debug: process.env.NEXT_PUBLIC_ENV !== "prod",
interpolation: {
escapeValue: false,
},
});

View File

@ -1,4 +1,3 @@
/* eslint-disable */
"use client";
import { createContext, useState } from "react";
@ -15,7 +14,11 @@ export const ChatsContext = createContext<ChatsContextType | undefined>(
undefined
);
export const ChatsProvider = ({ children }: { children: React.ReactNode }) => {
export const ChatsProvider = ({
children,
}: {
children: React.ReactNode;
}): JSX.Element => {
const [allChats, setAllChats] = useState<ChatEntity[]>([]);
return (

View File

@ -0,0 +1,14 @@
import { useContext } from "react";
import { KnowledgeContext } from "../knowledge-provider";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useKnowledgeContext = () => {
const context = useContext(KnowledgeContext);
if (context === undefined) {
throw new Error("useKnowledge must be used inside KnowledgeProvider");
}
return context;
};

View File

@ -0,0 +1 @@
export * from "./knowledge-provider";

View File

@ -0,0 +1,33 @@
"use client";
import { createContext, useState } from "react";
import { Knowledge } from "@/lib/types/Knowledge";
type KnowledgeContextType = {
allKnowledge: Knowledge[];
setAllKnowledge: React.Dispatch<React.SetStateAction<Knowledge[]>>;
};
export const KnowledgeContext = createContext<KnowledgeContextType | undefined>(
undefined
);
export const KnowledgeProvider = ({
children,
}: {
children: React.ReactNode;
}): JSX.Element => {
const [allKnowledge, setAllKnowledge] = useState<Knowledge[]>([]);
return (
<KnowledgeContext.Provider
value={{
allKnowledge,
setAllKnowledge,
}}
>
{children}
</KnowledgeContext.Provider>
);
};

View File

@ -1,3 +1,4 @@
export * from "./BrainProvider";
export * from "./ChatProvider";
export * from "./FeatureFlagProvider";
export * from "./KnowledgeProvider";

View File

@ -0,0 +1,23 @@
import { UUID } from "crypto";
export type Knowledge = UploadedKnowledge | CrawledKnowledge;
export interface UploadedKnowledge {
id: UUID;
brainId: UUID;
fileName: string;
extension: string;
}
export interface CrawledKnowledge {
id: UUID;
brainId: UUID;
url: string;
extension: string;
}
export const isUploadedKnowledge = (
knowledge: Knowledge
): knowledge is UploadedKnowledge => {
return "fileName" in knowledge && !("url" in knowledge);
};

View File

@ -1,5 +0,0 @@
const About = (): JSX.Element => {
return <div>About</div>;
};
export default About;

View File

@ -1,15 +1,15 @@
{
"title": "Explore uploaded data",
"subtitle": "View or delete stored data used by your brain",
"empty": "Oh No, Your Brain is empty.",
"noBrain": "Brain id not found",
"sessionNotFound": "User session not found",
"deleted": "{{fileName}} deleted from brain {{brain}}",
"errorDeleting": "Error deleting {{fileName}}",
"view": "View",
"chunkNumber": "No. of chunks: {{quantity}}",
"notAvailable": "Not Available",
"deleteConfirmTitle": "Confirm",
"deleteConfirmText": "Do you really want to delete?",
"feed_brain_instructions":"To add knowledge to a brain, go to chat page then click on plus button on the left of the chat input"
}
"title": "Explore uploaded data",
"subtitle": "View, download or delete knowledge used by your brain",
"empty": "Oh No, Your Brain is empty.",
"noBrain": "Brain id not found",
"sessionNotFound": "User session not found",
"deleted": "{{fileName}} deleted from brain {{brain}}",
"errorDeleting": "Error deleting {{fileName}}",
"view": "View",
"chunkNumber": "No. of chunks: {{quantity}}",
"notAvailable": "Not Available",
"deleteConfirmTitle": "Confirm",
"deleteConfirmText": "Do you really want to delete?",
"feed_brain_instructions": "To add knowledge to a brain, go to chat page then click on plus button on the left of the chat input"
}

View File

@ -1,15 +1,15 @@
{
"chunkNumber": "No. de partes: {{quantity}}",
"deleteConfirmText": "¿Realmente quieres eliminar?",
"deleteConfirmTitle": "Confirmar",
"deleted": "{{fileName}} borrado del cerebro {{brain}}",
"empty": "¡Oh No!, Tu Cerebro está vacío",
"errorDeleting": "Error borrando {{fileName}}",
"noBrain": "Id de Cerebro no encontrado",
"notAvailable": "No disponible",
"sessionNotFound": "Sesión no encontrada",
"subtitle": "Ver o borrar datos guardados usados por tu cerebro",
"title": "Explora datos subidos",
"view": "Ver",
"feed_brain_instructions": "Para agregar conocimiento a un cerebro, ve a la página de chat y haz clic en el botón de más a la izquierda del campo de chat."
}
"chunkNumber": "No. de partes: {{quantity}}",
"deleteConfirmText": "¿Realmente quieres eliminar?",
"deleteConfirmTitle": "Confirmar",
"deleted": "{{fileName}} borrado del cerebro {{brain}}",
"empty": "¡Oh No!, Tu Cerebro está vacío",
"errorDeleting": "Error borrando {{fileName}}",
"noBrain": "Id de Cerebro no encontrado",
"notAvailable": "No disponible",
"sessionNotFound": "Sesión no encontrada",
"subtitle": "Visualiza, descarga o elimina el conocimiento utilizado por tu cerebro",
"title": "Explora datos subidos",
"view": "Ver",
"feed_brain_instructions": "Para agregar conocimiento a un cerebro, ve a la página de chat y haz clic en el botón de más a la izquierda del campo de chat."
}

View File

@ -1,15 +1,15 @@
{
"title": "Explorez les données téléchargées",
"subtitle": "Visualisez ou supprimez les données stockées utilisées par votre cerveau",
"empty": "Oh non, votre cerveau est vide.",
"noBrain": "ID du cerveau introuvable",
"sessionNotFound": "Session utilisateur introuvable",
"deleted": "{{fileName}} supprimé du cerveau {{brain}}",
"errorDeleting": "Erreur lors de la suppression de {{fileName}}",
"view": "Voir",
"chunkNumber": "Nombre de fragments : {{quantity}}",
"notAvailable": "Non disponible",
"deleteConfirmTitle": "Confirmer",
"deleteConfirmText": "Voulez-vous vraiment supprimer ?",
"feed_brain_instructions": "Pour ajouter des connaissances à un cerveau, allez sur la page de chat, puis cliquez sur le bouton plus à gauche de la zone de chat."
}
"title": "Explorez les données téléchargées",
"subtitle": "Visualisez, téléchargez ou supprimez les connaissances utilisées par votre cerveau",
"empty": "Oh non, votre cerveau est vide.",
"noBrain": "ID du cerveau introuvable",
"sessionNotFound": "Session utilisateur introuvable",
"deleted": "{{fileName}} supprimé du cerveau {{brain}}",
"errorDeleting": "Erreur lors de la suppression de {{fileName}}",
"view": "Voir",
"chunkNumber": "Nombre de fragments : {{quantity}}",
"notAvailable": "Non disponible",
"deleteConfirmTitle": "Confirmer",
"deleteConfirmText": "Voulez-vous vraiment supprimer ?",
"feed_brain_instructions": "Pour ajouter des connaissances à un cerveau, allez sur la page de chat, puis cliquez sur le bouton plus à gauche de la zone de chat."
}

View File

@ -1,17 +1,15 @@
{
"title": "Explorar dados enviados",
"subtitle": "Visualize ou exclua dados armazenados usados pelo seu cérebro",
"empty": "Oh, não! Seu Cérebro está vazio.",
"noBrain": "Cérebro não encontrado",
"sessionNotFound": "Sessão do usuário não encontrada",
"deleted": "{{fileName}} excluído do cérebro {{brain}}",
"errorDeleting": "Erro ao excluir {{fileName}}",
"view": "Visualizar",
"chunkNumber": "Número de partes: {{quantity}}",
"notAvailable": "Indisponível",
"deleteConfirmTitle": "Confirmar",
"deleteConfirmText": "Você realmente deseja excluir?",
"feed_brain_instructions": "Para adicionar conhecimento a um cérebro, vá para a página de chat e clique no botão de adição à esquerda da entrada de chat."
}
"title": "Explorar dados enviados",
"subtitle": "Visualize, baixe ou exclua o conhecimento usado pelo seu cérebro",
"empty": "Oh, não! Seu Cérebro está vazio.",
"noBrain": "Cérebro não encontrado",
"sessionNotFound": "Sessão do usuário não encontrada",
"deleted": "{{fileName}} excluído do cérebro {{brain}}",
"errorDeleting": "Erro ao excluir {{fileName}}",
"view": "Visualizar",
"chunkNumber": "Número de partes: {{quantity}}",
"notAvailable": "Indisponível",
"deleteConfirmTitle": "Confirmar",
"deleteConfirmText": "Você realmente deseja excluir?",
"feed_brain_instructions": "Para adicionar conhecimento a um cérebro, vá para a página de chat e clique no botão de adição à esquerda da entrada de chat."
}

View File

@ -1,15 +1,15 @@
{
"title": "Исследовать загруженные данные",
"subtitle": "Просмотрите или удалите сохраненные данные, используемые вашим мозгом",
"empty": "О нет, ваш мозг пуст.",
"noBrain": "Мозг не найден",
"sessionNotFound": "Сессия пользователя не найдена",
"deleted": "{{fileName}} удален из мозга {{brain}}",
"errorDeleting": "Ошибка при удалении {{fileName}}",
"view": "Просмотреть",
"chunkNumber": "Количество частей: {{quantity}}",
"notAvailable": "Не доступно",
"deleteConfirmTitle": "Подтвердите",
"deleteConfirmText": "Вы действительно хотите удалить?",
"feed_brain_instructions": "Чтобы добавить знания в мозг, перейдите на страницу чата, затем нажмите на кнопку плюс слева от поля ввода чата."
"title": "Исследовать загруженные данные",
"subtitle": "Просмотрите, загрузите или удалите знания, используемые вашим мозгом",
"empty": "О нет, ваш мозг пуст.",
"noBrain": "Мозг не найден",
"sessionNotFound": "Сессия пользователя не найдена",
"deleted": "{{fileName}} удален из мозга {{brain}}",
"errorDeleting": "Ошибка при удалении {{fileName}}",
"view": "Просмотреть",
"chunkNumber": "Количество частей: {{quantity}}",
"notAvailable": "Не доступно",
"deleteConfirmTitle": "Подтвердите",
"deleteConfirmText": "Вы действительно хотите удалить?",
"feed_brain_instructions": "Чтобы добавить знания в мозг, перейдите на страницу чата, затем нажмите на кнопку плюс слева от поля ввода чата."
}

View File

@ -1,15 +1,15 @@
{
"title": "探索上传的数据",
"subtitle": "查看或删除大脑存储的数据",
"empty": "哎呀,你的大脑空空如也.",
"noBrain": "没有找到大脑",
"sessionNotFound": "未找到用户会话",
"deleted": "从大脑 {{brain}} 中删除 {{fileName}}。",
"errorDeleting": "删除 {{fileName}} 时发生错误",
"view": "查看",
"chunkNumber": "No. of chunks: {{quantity}}",
"notAvailable": "不可用",
"deleteConfirmTitle": "提交",
"deleteConfirmText": "你真的要删除吗?",
"feed_brain_instructions": "要向大脑添加知识,请转到聊天页面,然后单击聊天输入框左侧的加号按钮。"
}
"title": "探索上传的数据",
"subtitle": "查看、下载或删除您的大脑使用的知识",
"empty": "哎呀,你的大脑空空如也.",
"noBrain": "没有找到大脑",
"sessionNotFound": "未找到用户会话",
"deleted": "从大脑 {{brain}} 中删除 {{fileName}}。",
"errorDeleting": "删除 {{fileName}} 时发生错误",
"view": "查看",
"chunkNumber": "No. of chunks: {{quantity}}",
"notAvailable": "不可用",
"deleteConfirmTitle": "提交",
"deleteConfirmText": "你真的要删除吗?",
"feed_brain_instructions": "要向大脑添加知识,请转到聊天页面,然后单击聊天输入框左侧的加号按钮。"
}