feat(frontend): new modal for add knowledge (#2173)

# Description

Please include a summary of the changes and the related issue. Please
also include relevant motivation and context.

## Checklist before requesting a review

Please delete options that are not relevant.

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented hard-to-understand areas
- [ ] I have ideally added tests that prove my fix is effective or that
my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged

## Screenshots (if appropriate):
This commit is contained in:
Antoine Dewez 2024-02-10 16:17:05 -08:00 committed by GitHub
parent 70b2b58018
commit cf61ce8132
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 669 additions and 124 deletions

View File

@ -0,0 +1,62 @@
@use "@/styles/Colors.module.scss";
@use "@/styles/ScreenSizes.module.scss";
@use "@/styles/Spacings.module.scss";
@use "@/styles/Typography.module.scss";
.knowledge_to_feed_wrapper {
display: flex;
flex-direction: column;
padding-block: Spacings.$spacing05;
width: 100%;
gap: Spacings.$spacing05;
.single_selector_wrapper {
width: 30%;
min-width: 250px;
@media (max-width: ScreenSizes.$small) {
width: 100%;
}
}
.tabs_content_wrapper {
width: 100%;
height: 200px;
}
.uploaded_knowledges_title {
color: Colors.$dark-grey;
display: flex;
justify-content: space-between;
}
.uploaded_knowledges {
padding: Spacings.$spacing03;
display: flex;
width: 100%;
overflow: scroll;
flex-direction: column;
gap: Spacings.$spacing02;
overflow: scroll;
.uploaded_knowledge {
display: flex;
gap: Spacings.$spacing02;
align-items: center;
justify-content: space-between;
width: 100%;
overflow: hidden;
.left {
display: flex;
align-items: center;
gap: Spacings.$spacing02;
overflow: hidden;
.label {
@include Typography.EllipsisOverflow;
}
}
}
}
}

View File

@ -1,70 +1,99 @@
import Link from "next/link"; import { useMemo, useState } from "react";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { ApiBrainSecretsInputs } from "@/lib/components/ApiBrainSecretsInputs/ApiBrainSecretsInputs"; import { Icon } from "@/lib/components/ui/Icon/Icon";
import { KnowledgeToFeedInput } from "@/lib/components/KnowledgeToFeedInput"; import { SingleSelector } from "@/lib/components/ui/SingleSelector/SingleSelector";
import Button from "@/lib/components/ui/Button"; import { Tabs } from "@/lib/components/ui/Tabs/Tabs";
import { Select } from "@/lib/components/ui/Select";
import { requiredRolesForUpload } from "@/lib/config/upload"; import { requiredRolesForUpload } from "@/lib/config/upload";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext"; import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext"; import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
import { Tab } from "@/lib/types/Tab";
import { useFeedBrainInChat } from "./hooks/useFeedBrainInChat"; import styles from "./KnowledgeToFeed.module.scss";
import { FromDocuments } from "./components/FromDocuments/FromDocuments";
import { FromWebsites } from "./components/FromWebsites/FromWebsites";
import { formatMinimalBrainsToSelectComponentInput } from "./utils/formatMinimalBrainsToSelectComponentInput"; import { formatMinimalBrainsToSelectComponentInput } from "./utils/formatMinimalBrainsToSelectComponentInput";
type KnowledgeToFeedProps = { export const KnowledgeToFeed = (): JSX.Element => {
dispatchHasPendingRequests: () => void; const { allBrains, setCurrentBrainId, currentBrain } = useBrainContext();
}; const [selectedTab, setSelectedTab] = useState("From documents");
export const KnowledgeToFeed = ({ const { knowledgeToFeed, removeKnowledgeToFeed } =
dispatchHasPendingRequests, useKnowledgeToFeedContext();
}: KnowledgeToFeedProps): JSX.Element => {
const { allBrains, currentBrainId, setCurrentBrainId } = useBrainContext();
const { t } = useTranslation(["upload", "brain"]); const brainsWithUploadRights = formatMinimalBrainsToSelectComponentInput(
useMemo(
const { setShouldDisplayFeedCard } = useKnowledgeToFeedContext(); () =>
const { currentBrainDetails } = useBrainContext(); allBrains.filter((brain) =>
const brainsWithUploadRights = useMemo( requiredRolesForUpload.includes(brain.role)
() => ),
allBrains.filter((brain) => requiredRolesForUpload.includes(brain.role)), [allBrains]
[allBrains] )
); );
const { feedBrain } = useFeedBrainInChat({ const knowledgesTabs: Tab[] = [
dispatchHasPendingRequests, {
}); label: "From documents",
isSelected: selectedTab === "From documents",
onClick: () => setSelectedTab("From documents"),
iconName: "file",
},
{
label: "From websites",
isSelected: selectedTab === "From websites",
onClick: () => setSelectedTab("From websites"),
iconName: "website",
},
];
return ( return (
<div className="flex-col w-full relative pt-3" data-testid="feed-card"> <div className={styles.knowledge_to_feed_wrapper}>
<div className="flex justify-center"> <div className={styles.single_selector_wrapper}>
<Select <SingleSelector
options={formatMinimalBrainsToSelectComponentInput( options={brainsWithUploadRights}
brainsWithUploadRights onChange={setCurrentBrainId}
)} selectedOption={
emptyLabel={t("selected_brain_select_label")} currentBrain
value={currentBrainId ?? undefined} ? { label: currentBrain.name, value: currentBrain.id }
onChange={(newSelectedBrainId) => : undefined
setCurrentBrainId(newSelectedBrainId)
} }
className="flex flex-row items-center" placeholder="Select a brain"
/> />
</div> </div>
{currentBrainDetails?.brain_type === "api" ? ( <Tabs tabList={knowledgesTabs} />
<ApiBrainSecretsInputs <div className={styles.tabs_content_wrapper}>
brainId={currentBrainDetails.id} {selectedTab === "From documents" && <FromDocuments />}
onUpdate={() => setShouldDisplayFeedCard(false)} {selectedTab === "From websites" && <FromWebsites />}
/> </div>
) : ( <div>
<KnowledgeToFeedInput feedBrain={() => void feedBrain()} /> <div className={styles.uploaded_knowledges_title}>
)} <span>Uploaded knowledges</span>
{Boolean(currentBrainId) && ( <span>{knowledgeToFeed.length}</span>
<Link href={`/studio/${currentBrainId ?? ""}`}> </div>
<Button variant={"tertiary"}> <div className={styles.uploaded_knowledges}>
{t("manage_brain", { ns: "brain" })} {knowledgeToFeed.map((knowledge, index) => (
</Button> <div className={styles.uploaded_knowledge} key={index}>
</Link> <div className={styles.left}>
)} <Icon
name={knowledge.source === "crawl" ? "website" : "file"}
size="small"
color="black"
/>
<span className={styles.label}>
{knowledge.source === "crawl"
? knowledge.url
: knowledge.file.name}
</span>
</div>
<Icon
name="delete"
size="normal"
color="dangerous"
handleHover={true}
onClick={() => removeKnowledgeToFeed(index)}
/>
</div>
))}
</div>
</div>
</div> </div>
); );
}; };

View File

@ -0,0 +1,32 @@
@use "@/styles/Colors.module.scss";
@use "@/styles/Radius.module.scss";
@use "@/styles/Spacings.module.scss";
.from_document_wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
column-gap: Spacings.$spacing05;
justify-content: center;
align-items: center;
border: 1px dashed Colors.$lighter-grey;
border-radius: Radius.$big;
box-sizing: border-box;
&.dragging {
border: 3px dashed Colors.$accent;
background-color: Colors.$lightest-black;
}
.input {
padding: Spacings.$spacing05;
display: flex;
gap: Spacings.$spacing02;
.clickable {
cursor: pointer;
font-weight: bold;
}
}
}

View File

@ -0,0 +1,39 @@
import { useEffect, useState } from "react";
import { Icon } from "@/lib/components/ui/Icon/Icon";
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
import { useCustomDropzone } from "@/lib/hooks/useDropzone";
import styles from "./FromDocuments.module.scss";
export const FromDocuments = (): JSX.Element => {
const [dragging, setDragging] = useState<boolean>(false);
const { getRootProps, getInputProps, open } = useCustomDropzone();
const { knowledgeToFeed } = useKnowledgeToFeedContext();
useEffect(() => {
setDragging(false);
}, [knowledgeToFeed]);
return (
<div
className={`
${styles.from_document_wrapper}
${dragging ? styles.dragging : ""}
`}
{...getRootProps()}
onDragOver={() => setDragging(true)}
onDragLeave={() => setDragging(false)}
onMouseLeave={() => setDragging(false)}
>
<Icon name="upload" size="big" color={dragging ? "accent" : "black"} />
<div className={styles.input} onClick={open}>
<div className={styles.clickable}>
<span>Choose files</span>
<input {...getInputProps()} />
</div>
<span>or drag it here</span>
</div>
</div>
);
};

View File

@ -0,0 +1,20 @@
import { useCrawler } from "@/lib/components/KnowledgeToFeedInput/components/Crawler/hooks/useCrawler";
import { TextInput } from "@/lib/components/ui/TextInput/TextInput";
import styles from "./FromWebsites.module.scss";
export const FromWebsites = (): JSX.Element => {
const { handleSubmit, urlToCrawl, setUrlToCrawl } = useCrawler();
return (
<div className={styles.from_document_wrapper}>
<TextInput
label="Enter a website URL"
setInputValue={setUrlToCrawl}
inputValue={urlToCrawl}
iconName="followUp"
onSubmit={() => handleSubmit()}
/>
</div>
);
};

View File

@ -48,7 +48,7 @@ const SelectedChatPage = (): JSX.Element => {
onClick: () => { onClick: () => {
setShouldDisplayFeedCard(true); setShouldDisplayFeedCard(true);
}, },
iconName: "upload", iconName: "uploadFile",
}, },
{ {
label: "Manage current brain", label: "Manage current brain",

View File

@ -8,7 +8,6 @@ import { useBrainCreationContext } from "@/lib/components/AddBrainModal/componen
import PageHeader from "@/lib/components/PageHeader/PageHeader"; import PageHeader from "@/lib/components/PageHeader/PageHeader";
import { UploadDocumentModal } from "@/lib/components/UploadDocumentModal/UploadDocumentModal"; import { UploadDocumentModal } from "@/lib/components/UploadDocumentModal/UploadDocumentModal";
import { SearchBar } from "@/lib/components/ui/SearchBar/SearchBar"; import { SearchBar } from "@/lib/components/ui/SearchBar/SearchBar";
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
import { useSupabase } from "@/lib/context/SupabaseProvider"; import { useSupabase } from "@/lib/context/SupabaseProvider";
import { redirectToLogin } from "@/lib/router/redirectToLogin"; import { redirectToLogin } from "@/lib/router/redirectToLogin";
import { ButtonType } from "@/lib/types/QuivrButton"; import { ButtonType } from "@/lib/types/QuivrButton";
@ -18,7 +17,6 @@ import styles from "./page.module.scss";
const Search = (): JSX.Element => { const Search = (): JSX.Element => {
const pathname = usePathname(); const pathname = usePathname();
const { session } = useSupabase(); const { session } = useSupabase();
const { setShouldDisplayFeedCard } = useKnowledgeToFeedContext();
const { setIsBrainCreationModalOpened } = useBrainCreationContext(); const { setIsBrainCreationModalOpened } = useBrainCreationContext();
useEffect(() => { useEffect(() => {
@ -36,14 +34,6 @@ const Search = (): JSX.Element => {
}, },
iconName: "brain", iconName: "brain",
}, },
{
label: "Add knowledge",
color: "primary",
onClick: () => {
setShouldDisplayFeedCard(true);
},
iconName: "upload",
},
]; ];
return ( return (

View File

@ -2,6 +2,7 @@ import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useChatApi } from "@/lib/api/chat/useChatApi"; import { useChatApi } from "@/lib/api/chat/useChatApi";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext"; import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
import { useToast } from "@/lib/hooks"; import { useToast } from "@/lib/hooks";
import { useUrlBrain } from "@/lib/hooks/useBrainIdFromUrl"; import { useUrlBrain } from "@/lib/hooks/useBrainIdFromUrl";
@ -18,13 +19,17 @@ export const useFeedBrain = ({
}) => { }) => {
const { publish } = useToast(); const { publish } = useToast();
const { t } = useTranslation(["upload"]); const { t } = useTranslation(["upload"]);
const { brainId } = useUrlBrain(); let { brainId } = useUrlBrain();
const { setKnowledgeToFeed, knowledgeToFeed } = useKnowledgeToFeedContext(); const { currentBrainId } = useBrainContext();
const { setKnowledgeToFeed, knowledgeToFeed, setShouldDisplayFeedCard } =
useKnowledgeToFeedContext();
const [hasPendingRequests, setHasPendingRequests] = useState(false); const [hasPendingRequests, setHasPendingRequests] = useState(false);
const { handleFeedBrain } = useFeedBrainHandler(); const { handleFeedBrain } = useFeedBrainHandler();
const { createChat, deleteChat } = useChatApi(); const { createChat, deleteChat } = useChatApi();
const feedBrain = async (): Promise<void> => { const feedBrain = async (): Promise<void> => {
brainId ??= currentBrainId ?? undefined;
if (brainId === undefined) { if (brainId === undefined) {
publish({ publish({
variant: "danger", variant: "danger",
@ -50,6 +55,7 @@ export const useFeedBrain = ({
dispatchHasPendingRequests?.(); dispatchHasPendingRequests?.();
closeFeedInput?.(); closeFeedInput?.();
setHasPendingRequests(true); setHasPendingRequests(true);
setShouldDisplayFeedCard(false);
await handleFeedBrain({ await handleFeedBrain({
brainId, brainId,
chatId: currentChatId, chatId: currentChatId,

View File

@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Button from "@/lib/components/ui/Button"; import Button from "@/lib/components/ui/Button";
import { Modal } from "@/lib/components/ui/Modal"; import { Modal } from "@/lib/components/ui/Modal/Modal";
type DeleteOrUnsubscribeConfirmationModalProps = { type DeleteOrUnsubscribeConfirmationModalProps = {
isOpen: boolean; isOpen: boolean;

View File

@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Button from "@/lib/components/ui/Button"; import Button from "@/lib/components/ui/Button";
import { Modal } from "@/lib/components/ui/Modal"; import { Modal } from "@/lib/components/ui/Modal/Modal";
import { useBrainFormState } from "../../hooks/useBrainFormState"; import { useBrainFormState } from "../../hooks/useBrainFormState";

View File

@ -50,7 +50,7 @@ const Studio = (): JSX.Element => {
onClick: () => { onClick: () => {
setShouldDisplayFeedCard(true); setShouldDisplayFeedCard(true);
}, },
iconName: "upload", iconName: "uploadFile",
}, },
]; ];

View File

@ -4,7 +4,7 @@ import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import PageHeader from "@/lib/components/PageHeader/PageHeader"; import PageHeader from "@/lib/components/PageHeader/PageHeader";
import { Modal } from "@/lib/components/ui/Modal"; import { Modal } from "@/lib/components/ui/Modal/Modal";
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton"; import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
import { Tabs } from "@/lib/components/ui/Tabs/Tabs"; import { Tabs } from "@/lib/components/ui/Tabs/Tabs";
import { useSupabase } from "@/lib/context/SupabaseProvider"; import { useSupabase } from "@/lib/context/SupabaseProvider";

View File

@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Modal } from "@/lib/components/ui/Modal"; import { Modal } from "@/lib/components/ui/Modal/Modal";
import { useBrainCreationContext } from "./brainCreation-provider"; import { useBrainCreationContext } from "./brainCreation-provider";
import { BrainKnowledgeStep } from "./components/BrainKnowledgeStep/BrainKnowledgeStep"; import { BrainKnowledgeStep } from "./components/BrainKnowledgeStep/BrainKnowledgeStep";

View File

@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Button from "@/lib/components/ui/Button"; import Button from "@/lib/components/ui/Button";
import { Modal } from "@/lib/components/ui/Modal"; import { Modal } from "@/lib/components/ui/Modal/Modal";
type PublicAccessConfirmationModalProps = { type PublicAccessConfirmationModalProps = {
opened: boolean; opened: boolean;

View File

@ -1,9 +1,18 @@
@use "@/styles/Colors.module.scss"; @use "@/styles/Colors.module.scss";
@use "@/styles/Spacings.module.scss";
@use "@/styles/ZIndexes.module.scss"; @use "@/styles/ZIndexes.module.scss";
.knowledge_modal { .knowledge_modal {
position: relative;
display: flex; display: flex;
flex-direction: column;
justify-content: space-between;
background-color: Colors.$white; background-color: Colors.$white;
align-items: center; width: 100%;
justify-content: center; flex: 1;
.button {
display: flex;
justify-content: flex-end;
}
} }

View File

@ -1,17 +1,32 @@
import { AnimatePresence, motion } from "framer-motion"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { KnowledgeToFeed } from "@/app/chat/[chatId]/components/ActionsBar/components"; import { KnowledgeToFeed } from "@/app/chat/[chatId]/components/ActionsBar/components";
import { useActionBar } from "@/app/chat/[chatId]/components/ActionsBar/hooks/useActionBar"; import { useAddKnowledge } from "@/app/studio/[brainId]/components/BrainManagementTabs/components/KnowledgeOrSecretsTab/components/AddKnowledge/hooks/useAddKnowledge";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext"; import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
import styles from "./UploadDocumentModal.module.scss"; import styles from "./UploadDocumentModal.module.scss";
import { Modal } from "../ui/Modal"; import { Modal } from "../ui/Modal/Modal";
import { QuivrButton } from "../ui/QuivrButton/QuivrButton";
export const UploadDocumentModal = (): JSX.Element => { export const UploadDocumentModal = (): JSX.Element => {
const { shouldDisplayFeedCard, setShouldDisplayFeedCard } = const { shouldDisplayFeedCard, setShouldDisplayFeedCard, knowledgeToFeed } =
useKnowledgeToFeedContext(); useKnowledgeToFeedContext();
const { setHasPendingRequests } = useActionBar(); const { currentBrain } = useBrainContext();
const { feedBrain } = useAddKnowledge();
const [feeding, setFeeding] = useState<boolean>(false);
useKnowledgeToFeedContext();
const { t } = useTranslation(["knowledge"]);
const handleFeedBrain = async () => {
setFeeding(true);
await feedBrain();
setFeeding(false);
setShouldDisplayFeedCard(false);
};
if (!shouldDisplayFeedCard) { if (!shouldDisplayFeedCard) {
return <></>; return <></>;
@ -21,21 +36,23 @@ export const UploadDocumentModal = (): JSX.Element => {
<Modal <Modal
isOpen={shouldDisplayFeedCard} isOpen={shouldDisplayFeedCard}
setOpen={setShouldDisplayFeedCard} setOpen={setShouldDisplayFeedCard}
title={t("addKnowledgeTitle", { ns: "knowledge" })}
desc={t("addKnowledgeSubtitle", { ns: "knowledge" })}
bigModal={true}
CloseTrigger={<div />} CloseTrigger={<div />}
> >
<div className={styles.knowledge_modal}> <div className={styles.knowledge_modal}>
<AnimatePresence> <KnowledgeToFeed />
<motion.div <div className={styles.button}>
key="slide" <QuivrButton
initial={{ y: "100%", opacity: 0 }} label="Feed Brain"
animate={{ y: 0, opacity: 1, transition: { duration: 0.2 } }} color="primary"
exit={{ y: "100%", opacity: 0 }} iconName="add"
> onClick={handleFeedBrain}
<KnowledgeToFeed disabled={knowledgeToFeed.length === 0 || !currentBrain}
dispatchHasPendingRequests={() => setHasPendingRequests(true)} isLoading={feeding}
/> />
</motion.div> </div>
</AnimatePresence>
</div> </div>
</Modal> </Modal>
); );

View File

@ -2,23 +2,31 @@
@use "@/styles/IconSizes.module.scss"; @use "@/styles/IconSizes.module.scss";
.small { .small {
width: IconSizes.$small; min-width: IconSizes.$small;
height: IconSizes.$small; min-height: IconSizes.$small;
max-width: IconSizes.$small;
max-height: IconSizes.$small;
} }
.normal { .normal {
width: IconSizes.$normal; min-width: IconSizes.$normal;
height: IconSizes.$normal; min-height: IconSizes.$normal;
max-width: IconSizes.$normal;
max-height: IconSizes.$normal;
} }
.large { .large {
width: IconSizes.$large; min-width: IconSizes.$large;
height: IconSizes.$large; min-height: IconSizes.$large;
max-width: IconSizes.$large;
max-height: IconSizes.$large;
} }
.big { .big {
width: IconSizes.$big; min-width: IconSizes.$big;
height: IconSizes.$big; min-height: IconSizes.$big;
max-width: IconSizes.$big;
max-height: IconSizes.$big;
} }
.black { .black {

View File

@ -0,0 +1,49 @@
@use "@/styles/Colors.module.scss";
@use "@/styles/Radius.module.scss";
@use "@/styles/ScreenSizes.module.scss";
@use "@/styles/Spacings.module.scss";
@use "@/styles/ZIndexes.module.scss";
.modal_container {
display: flex;
background-color: rgba(Colors.$dark-black, 0.94);
height: 100%;
width: 100%;
position: absolute;
z-index: ZIndexes.$modal;
align-items: center;
justify-content: center;
.modal_content_wrapper {
display: flex;
flex-direction: column;
border-radius: Radius.$big;
background-color: Colors.$white;
padding: Spacings.$spacing05;
cursor: auto;
box-shadow: 0 2px 4px rgb(0, 0, 0, 0.25);
max-width: 90vw;
overflow: scroll;
&.big_modal {
width: 50vw;
height: 90vh;
}
@media (max-width: ScreenSizes.$small) {
&.big_modal {
width: 90vw;
}
}
.close_button_wrapper {
display: inline-flex;
position: absolute;
top: 0;
right: 0;
padding: Spacings.$spacing05;
border-radius: 50;
outline: none;
}
}
}

View File

@ -7,7 +7,9 @@ import { ReactNode, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { MdClose } from "react-icons/md"; import { MdClose } from "react-icons/md";
import Button from "./Button"; import styles from "./Modal.module.scss";
import Button from "../Button";
type CommonModalProps = { type CommonModalProps = {
title?: string; title?: string;
@ -17,6 +19,7 @@ type CommonModalProps = {
CloseTrigger?: ReactNode; CloseTrigger?: ReactNode;
isOpen?: undefined; isOpen?: undefined;
setOpen?: undefined; setOpen?: undefined;
bigModal?: boolean;
}; };
type ModalProps = type ModalProps =
@ -34,6 +37,7 @@ export const Modal = ({
CloseTrigger, CloseTrigger,
isOpen: customIsOpen, isOpen: customIsOpen,
setOpen: customSetOpen, setOpen: customSetOpen,
bigModal,
}: ModalProps): JSX.Element => { }: ModalProps): JSX.Element => {
const [isOpen, setOpen] = useState(false); const [isOpen, setOpen] = useState(false);
const { t } = useTranslation(["translation"]); const { t } = useTranslation(["translation"]);
@ -51,17 +55,19 @@ export const Modal = ({
<Dialog.Portal forceMount> <Dialog.Portal forceMount>
<Dialog.Overlay asChild forceMount> <Dialog.Overlay asChild forceMount>
<motion.div <motion.div
className="z-[10000] py-20 fixed inset-0 flex justify-center overflow-auto cursor-pointer bg-black/50 backdrop-blur-sm" className={styles.modal_container}
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
> >
<Dialog.Content asChild forceMount> <Dialog.Content asChild forceMount>
<motion.div <motion.div
className={`${styles.modal_content_wrapper} ${
bigModal ? styles.big_modal : ""
}`}
initial={{ opacity: 0, y: "-40%" }} initial={{ opacity: 0, y: "-40%" }}
animate={{ opacity: 1, y: "0%" }} animate={{ opacity: 1, y: "0%" }}
exit={{ opacity: 0, y: "40%" }} exit={{ opacity: 0, y: "40%" }}
className="w-[90vw] my-auto flex flex-col h-fit max-w-2xl rounded-xl bg-white dark:bg-black border border-black/10 dark:border-white/25 p-10 shadow-xl dark:shadow-primary/50 focus:outline-none cursor-auto"
> >
<Dialog.Title <Dialog.Title
className="m-0 text-2xl font-bold" className="m-0 text-2xl font-bold"
@ -87,7 +93,7 @@ export const Modal = ({
</Dialog.Close> </Dialog.Close>
<Dialog.Close asChild> <Dialog.Close asChild>
<button <button
className="absolute top-0 p-5 right-0 inline-flex appearance-none items-center justify-center rounded-full focus:shadow-sm focus:outline-none" className={styles.close_button_wrapper}
aria-label="Close" aria-label="Close"
> >
<MdClose /> <MdClose />

View File

@ -13,6 +13,7 @@
border: 1.5px solid transparent; border: 1.5px solid transparent;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
width: fit-content;
&.primary { &.primary {
border-color: Colors.$primary; border-color: Colors.$primary;
@ -33,6 +34,12 @@
color: Colors.$white; color: Colors.$white;
} }
} }
&.disabled {
border-color: Colors.$normal-grey;
pointer-events: none;
color: Colors.$normal-grey;
}
} }
.icon_label { .icon_label {

View File

@ -13,29 +13,38 @@ export const QuivrButton = ({
color, color,
isLoading, isLoading,
iconName, iconName,
disabled,
}: ButtonType): JSX.Element => { }: ButtonType): JSX.Element => {
const [hovered, setHovered] = useState<boolean>(false); const [hovered, setHovered] = useState<boolean>(false);
return ( return (
<div <div
className={`${styles.button_wrapper} ${styles[color]}`} className={`
onClick={onClick} ${styles.button_wrapper}
${styles[color]}
${disabled ? styles.disabled : ""}
`}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick={() => onClick()}
onMouseEnter={() => setHovered(true)} onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)} onMouseLeave={() => setHovered(false)}
> >
{!isLoading ? ( <div className={styles.icon_label}>
<div className={styles.icon_label}> {!isLoading ? (
<Icon <Icon
name={iconName} name={iconName}
size="normal" size="normal"
color={hovered ? "white" : color} color={hovered ? "white" : disabled ? "grey" : color}
handleHover={false} handleHover={false}
/> />
<span className={styles.label}>{label}</span> ) : (
</div> <LoaderIcon
) : ( color={hovered ? "white" : disabled ? "grey" : color}
<LoaderIcon color="black" size="small" /> size="small"
)} />
)}
<span className={styles.label}>{label}</span>
</div>
</div> </div>
); );
}; };

View File

@ -0,0 +1,114 @@
@use "@/styles/Colors.module.scss";
@use "@/styles/IconSizes.module.scss";
@use "@/styles/Radius.module.scss";
@use "@/styles/Spacings.module.scss";
@use "@/styles/Typography.module.scss";
.single_selector_wrapper {
display: flex;
flex-direction: column;
position: relative;
.first_line_wrapper {
display: flex;
justify-content: space-between;
border: 1px solid Colors.$normal-grey;
border-radius: Radius.$normal;
align-items: center;
cursor: pointer;
&.unfolded {
border-radius: Radius.$normal Radius.$normal 0 0;
}
&:hover {
background-color: Colors.$lightest-grey;
}
.left {
display: flex;
align-items: center;
gap: Spacings.$spacing03;
padding: Spacings.$spacing03;
overflow: hidden;
flex: 1;
.icon {
width: IconSizes.$normal;
}
.label {
@include Typography.EllipsisOverflow;
color: Colors.$white;
background-color: Colors.$primary;
border-radius: Radius.$normal;
padding-inline: Spacings.$spacing05;
padding-block: Spacings.$spacing02;
white-space: nowrap;
&.not_set {
color: Colors.$normal-grey;
background-color: transparent;
padding-inline: 0;
}
&.unfolded_not_set {
width: 0;
}
}
}
.right {
flex: 1;
max-width: 50%;
&.folded {
display: none;
}
}
}
.options {
position: absolute;
background-color: Colors.$white;
width: 100%;
top: 100%;
border: 1px solid Colors.$normal-grey;
border-top: none;
border-radius: 0 0 Radius.$normal Radius.$normal;
overflow: hidden;
max-height: 180px;
overflow: scroll;
.option {
padding: Spacings.$spacing03;
cursor: pointer;
display: flex;
gap: Spacings.$spacing03;
align-items: center;
overflow: hidden;
&:hover {
background-color: Colors.$lightest-grey;
.brain_name {
background-color: Colors.$primary;
color: Colors.$white;
}
}
.icon {
width: IconSizes.$normal;
}
.brain_name {
@include Typography.EllipsisOverflow;
border: 1px solid Colors.$lightest-black;
border-radius: Radius.$small;
padding-inline: Spacings.$spacing05;
padding-block: Spacings.$spacing02;
white-space: nowrap;
}
}
}
}

View File

@ -0,0 +1,92 @@
import { UUID } from "crypto";
import { useState } from "react";
import styles from "./SingleSelector.module.scss";
import { Icon } from "../Icon/Icon";
import { TextInput } from "../TextInput/TextInput";
export type SelectOptionProps<T> = {
label: string;
value: T;
};
type SelectProps<T> = {
options: SelectOptionProps<T>[];
onChange: (option: T) => void;
selectedOption: SelectOptionProps<T> | undefined;
placeholder: string;
};
export const SingleSelector = <T extends string | number | UUID>({
onChange,
options,
selectedOption,
placeholder,
}: SelectProps<T>): JSX.Element => {
const [search, setSearch] = useState<string>("");
const [folded, setFolded] = useState<boolean>(true);
const filteredOptions = options.filter((option) =>
option.label.toLowerCase().includes(search.toLowerCase())
);
const handleOptionClick = (option: SelectOptionProps<T>) => {
onChange(option.value);
setFolded(true);
};
return (
<div className={styles.single_selector_wrapper}>
<div
className={`${styles.first_line_wrapper} ${
!folded ? styles.unfolded : ""
}`}
>
<div className={styles.left} onClick={() => setFolded(!folded)}>
<div className={styles.icon}>
<Icon
name={folded ? "chevronDown" : "chevronRight"}
size="normal"
color="black"
/>
</div>
<div
className={`
${styles.label}
${!selectedOption ? styles.not_set : ""}
`}
>
{selectedOption?.label ?? placeholder}
</div>
</div>
{!folded && (
<div className={styles.right}>
<TextInput
label="Search..."
inputValue={search}
setInputValue={setSearch}
simple={true}
/>
</div>
)}
</div>
{!folded && (
<div className={styles.options}>
{filteredOptions.map((option) => (
<div
className={styles.option}
key={option.value.toString()}
onClick={() => handleOptionClick(option)}
>
<div className={styles.icon}>
<Icon name="brain" size="normal" color="black" />
</div>
<span className={styles.brain_name}>{option.label}</span>
</div>
))}
</div>
)}
</div>
);
};

View File

@ -7,13 +7,25 @@
border: 1px solid Colors.$lighter-grey; border: 1px solid Colors.$lighter-grey;
gap: Spacings.$spacing03; gap: Spacings.$spacing03;
padding-block: Spacings.$spacing02; padding-block: Spacings.$spacing02;
padding-inline: Spacings.$spacing05; padding-inline: Spacings.$spacing03;
border-radius: Radius.$big; border-radius: Radius.$big;
align-items: center; align-items: center;
width: 100%;
&.simple {
border: none;
padding: 0;
.text_input {
background-color: transparent;
width: 10px;
}
}
.text_input { .text_input {
caret-color: Colors.$accent; caret-color: Colors.$accent;
border: none; border: none;
flex: 1;
&:focus { &:focus {
box-shadow: none; box-shadow: none;

View File

@ -3,10 +3,12 @@ import styles from "./TextInput.module.scss";
import { Icon } from "../Icon/Icon"; import { Icon } from "../Icon/Icon";
type TextInputProps = { type TextInputProps = {
iconName: string; iconName?: string;
label: string; label: string;
inputValue: string; inputValue: string;
setInputValue: (value: string) => void; setInputValue: (value: string) => void;
simple?: boolean;
onSubmit?: () => void;
}; };
export const TextInput = ({ export const TextInput = ({
@ -14,17 +16,36 @@ export const TextInput = ({
label, label,
inputValue, inputValue,
setInputValue, setInputValue,
simple,
onSubmit,
}: TextInputProps): JSX.Element => { }: TextInputProps): JSX.Element => {
return ( return (
<div className={styles.text_input_container}> <div
className={`
${styles.text_input_container}
${simple ? styles.simple : ""}
`}
>
<input <input
className={styles.text_input} className={styles.text_input}
type="text" type="text"
value={inputValue} value={inputValue}
onChange={(e) => setInputValue(e.target.value)} onChange={(e) => setInputValue(e.target.value)}
placeholder={label} placeholder={label}
onKeyDown={(e) => {
if (e.key === "Enter" && onSubmit) {
onSubmit();
}
}}
/> />
<Icon name={iconName} size="small" color="black" /> {!simple && iconName && (
<Icon
name={iconName}
size="normal"
color={onSubmit ? (inputValue ? "accent" : "grey") : "black"}
onClick={onSubmit}
/>
)}
</div> </div>
); );
}; };

View File

@ -9,6 +9,7 @@ import {
FaRegUserCircle, FaRegUserCircle,
FaUnlock, FaUnlock,
} from "react-icons/fa"; } from "react-icons/fa";
import { FiUpload } from "react-icons/fi";
import { IoIosAdd, IoMdClose, IoMdLogOut } from "react-icons/io"; import { IoIosAdd, IoMdClose, IoMdLogOut } from "react-icons/io";
import { import {
IoArrowUpCircleOutline, IoArrowUpCircleOutline,
@ -34,6 +35,7 @@ import {
MdUploadFile, MdUploadFile,
} from "react-icons/md"; } from "react-icons/md";
import { RiHashtag } from "react-icons/ri"; import { RiHashtag } from "react-icons/ri";
import { TbNetwork } from "react-icons/tb";
import { VscGraph } from "react-icons/vsc"; import { VscGraph } from "react-icons/vsc";
export const iconList: { [name: string]: IconType } = { export const iconList: { [name: string]: IconType } = {
@ -66,6 +68,8 @@ export const iconList: { [name: string]: IconType } = {
settings: IoSettingsSharp, settings: IoSettingsSharp,
star: FaRegStar, star: FaRegStar,
unlock: FaUnlock, unlock: FaUnlock,
upload: MdUploadFile, upload: FiUpload,
uploadFile: MdUploadFile,
user: FaRegUserCircle, user: FaRegUserCircle,
website: TbNetwork,
}; };

View File

@ -7,5 +7,6 @@ export interface ButtonType {
color: Color; color: Color;
isLoading?: boolean; isLoading?: boolean;
iconName: keyof typeof iconList; iconName: keyof typeof iconList;
onClick: () => void; onClick: () => void | Promise<void>;
disabled?: boolean;
} }

View File

@ -1 +1,4 @@
{} {
"addKnowledgeTitle": "Add Knowledge",
"addKnowledgeSubtitle": "Feed your brain with knowledges from documents or websites"
}

View File

@ -1 +1,4 @@
{} {
"addKnowledgeTitle": "Agregar Conocimiento",
"addKnowledgeSubtitle": "Alimenta tu cerebro con conocimientos de documentos o sitios web"
}

View File

@ -1 +1,4 @@
{} {
"addKnowledgeTitle": "Ajouter de la connaissance",
"addKnowledgeSubtitle": "Nourris ton cerveau avec de la connaissance venant de documents ou de sites internets"
}

View File

@ -1 +1,4 @@
{} {
"addKnowledgeTitle": "Adicionar Conhecimento",
"addKnowledgeSubtitle": "Alimente seu cérebro com conhecimento de documentos ou sites da internet"
}

View File

@ -1 +1,4 @@
{} {
"addKnowledgeTitle": "Добавить Знание",
"addKnowledgeSubtitle": "Пополните свой мозг знаниями из документов или веб-сайтов"
}

View File

@ -1 +1,4 @@
{} {
"addKnowledgeTitle": "添加知识",
"addKnowledgeSubtitle": "用文件或网站的知识充实你的大脑"
}