mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-12-15 01:21:48 +03:00
feat: knowledge tab list (#1222)
* ✨ 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:
parent
48bdbbb3e9
commit
d2b4ef4aff
backend
models
repository
files
knowledge
routes
frontend
app/brains-management
[brainId]/components/BrainManagementTabs/components/KnowledgeTab
DocumentItem
KnowledgeItem
CrawledKnowledgeItem.tsxDeleteKnowledge.tsxDownloadUploadedKnowledge.tsxUploadedKnowledgeItem.tsx
KnowledgeTable.tsx__tests__
index.tsxuseKnowledgeItem.tshooks
index.tsxlib
pages
public/locales
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
17
backend/repository/files/delete_file.py
Normal file
17
backend/repository/files/delete_file.py
Normal 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
|
@ -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
|
||||
|
14
backend/repository/knowledge/get_all_knowledge.py
Normal file
14
backend/repository/knowledge/get_all_knowledge.py
Normal 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
|
15
backend/repository/knowledge/get_knowledge.py
Normal file
15
backend/repository/knowledge/get_knowledge.py
Normal 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
|
16
backend/repository/knowledge/remove_knowledge.py
Normal file
16
backend/repository/knowledge/remove_knowledge.py
Normal 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
|
@ -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
|
||||
|
@ -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;
|
@ -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;
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
@ -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,
|
||||
};
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
6
frontend/lib/api/knowledge/config.ts
Normal file
6
frontend/lib/api/knowledge/config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { UUID } from "crypto";
|
||||
|
||||
const brainDataKey = "quivr-knowledge";
|
||||
|
||||
export const getKnowledgeDataKey = (knowledgeId: UUID): string =>
|
||||
`${brainDataKey}-${knowledgeId}`;
|
68
frontend/lib/api/knowledge/knowledge.ts
Normal file
68
frontend/lib/api/knowledge/knowledge.ts
Normal 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;
|
||||
};
|
25
frontend/lib/api/knowledge/useKnowledgeApi.ts
Normal file
25
frontend/lib/api/knowledge/useKnowledgeApi.ts
Normal 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),
|
||||
};
|
||||
};
|
@ -14,4 +14,7 @@ i18n.use(initReactI18next).init({
|
||||
defaultNS,
|
||||
resources,
|
||||
debug: process.env.NEXT_PUBLIC_ENV !== "prod",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
|
@ -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 (
|
||||
|
@ -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;
|
||||
};
|
1
frontend/lib/context/KnowledgeProvider/index.ts
Normal file
1
frontend/lib/context/KnowledgeProvider/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./knowledge-provider";
|
@ -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>
|
||||
);
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
export * from "./BrainProvider";
|
||||
export * from "./ChatProvider";
|
||||
export * from "./FeatureFlagProvider";
|
||||
export * from "./KnowledgeProvider";
|
||||
|
23
frontend/lib/types/Knowledge.ts
Normal file
23
frontend/lib/types/Knowledge.ts
Normal 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);
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
const About = (): JSX.Element => {
|
||||
return <div>About</div>;
|
||||
};
|
||||
|
||||
export default About;
|
@ -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"
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -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": "Чтобы добавить знания в мозг, перейдите на страницу чата, затем нажмите на кнопку плюс слева от поля ввода чата."
|
||||
}
|
||||
|
@ -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": "要向大脑添加知识,请转到聊天页面,然后单击聊天输入框左侧的加号按钮。"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user