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

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
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";

View File

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

View File

@ -4,7 +4,7 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
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 { Tabs } from "@/lib/components/ui/Tabs/Tabs";
import { useSupabase } from "@/lib/context/SupabaseProvider";

View File

@ -1,6 +1,6 @@
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 { BrainKnowledgeStep } from "./components/BrainKnowledgeStep/BrainKnowledgeStep";

View File

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

View File

@ -1,9 +1,18 @@
@use "@/styles/Colors.module.scss";
@use "@/styles/Spacings.module.scss";
@use "@/styles/ZIndexes.module.scss";
.knowledge_modal {
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: Colors.$white;
align-items: center;
justify-content: center;
width: 100%;
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 { 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 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 => {
const { shouldDisplayFeedCard, setShouldDisplayFeedCard } =
const { shouldDisplayFeedCard, setShouldDisplayFeedCard, knowledgeToFeed } =
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) {
return <></>;
@ -21,21 +36,23 @@ export const UploadDocumentModal = (): JSX.Element => {
<Modal
isOpen={shouldDisplayFeedCard}
setOpen={setShouldDisplayFeedCard}
title={t("addKnowledgeTitle", { ns: "knowledge" })}
desc={t("addKnowledgeSubtitle", { ns: "knowledge" })}
bigModal={true}
CloseTrigger={<div />}
>
<div className={styles.knowledge_modal}>
<AnimatePresence>
<motion.div
key="slide"
initial={{ y: "100%", opacity: 0 }}
animate={{ y: 0, opacity: 1, transition: { duration: 0.2 } }}
exit={{ y: "100%", opacity: 0 }}
>
<KnowledgeToFeed
dispatchHasPendingRequests={() => setHasPendingRequests(true)}
/>
</motion.div>
</AnimatePresence>
<KnowledgeToFeed />
<div className={styles.button}>
<QuivrButton
label="Feed Brain"
color="primary"
iconName="add"
onClick={handleFeedBrain}
disabled={knowledgeToFeed.length === 0 || !currentBrain}
isLoading={feeding}
/>
</div>
</div>
</Modal>
);

View File

@ -2,23 +2,31 @@
@use "@/styles/IconSizes.module.scss";
.small {
width: IconSizes.$small;
height: IconSizes.$small;
min-width: IconSizes.$small;
min-height: IconSizes.$small;
max-width: IconSizes.$small;
max-height: IconSizes.$small;
}
.normal {
width: IconSizes.$normal;
height: IconSizes.$normal;
min-width: IconSizes.$normal;
min-height: IconSizes.$normal;
max-width: IconSizes.$normal;
max-height: IconSizes.$normal;
}
.large {
width: IconSizes.$large;
height: IconSizes.$large;
min-width: IconSizes.$large;
min-height: IconSizes.$large;
max-width: IconSizes.$large;
max-height: IconSizes.$large;
}
.big {
width: IconSizes.$big;
height: IconSizes.$big;
min-width: IconSizes.$big;
min-height: IconSizes.$big;
max-width: IconSizes.$big;
max-height: IconSizes.$big;
}
.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 { MdClose } from "react-icons/md";
import Button from "./Button";
import styles from "./Modal.module.scss";
import Button from "../Button";
type CommonModalProps = {
title?: string;
@ -17,6 +19,7 @@ type CommonModalProps = {
CloseTrigger?: ReactNode;
isOpen?: undefined;
setOpen?: undefined;
bigModal?: boolean;
};
type ModalProps =
@ -34,6 +37,7 @@ export const Modal = ({
CloseTrigger,
isOpen: customIsOpen,
setOpen: customSetOpen,
bigModal,
}: ModalProps): JSX.Element => {
const [isOpen, setOpen] = useState(false);
const { t } = useTranslation(["translation"]);
@ -51,17 +55,19 @@ export const Modal = ({
<Dialog.Portal forceMount>
<Dialog.Overlay asChild forceMount>
<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 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<Dialog.Content asChild forceMount>
<motion.div
className={`${styles.modal_content_wrapper} ${
bigModal ? styles.big_modal : ""
}`}
initial={{ opacity: 0, y: "-40%" }}
animate={{ opacity: 1, y: "0%" }}
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
className="m-0 text-2xl font-bold"
@ -87,7 +93,7 @@ export const Modal = ({
</Dialog.Close>
<Dialog.Close asChild>
<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"
>
<MdClose />

View File

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

View File

@ -13,29 +13,38 @@ export const QuivrButton = ({
color,
isLoading,
iconName,
disabled,
}: ButtonType): JSX.Element => {
const [hovered, setHovered] = useState<boolean>(false);
return (
<div
className={`${styles.button_wrapper} ${styles[color]}`}
onClick={onClick}
className={`
${styles.button_wrapper}
${styles[color]}
${disabled ? styles.disabled : ""}
`}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick={() => onClick()}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{!isLoading ? (
<div className={styles.icon_label}>
<div className={styles.icon_label}>
{!isLoading ? (
<Icon
name={iconName}
size="normal"
color={hovered ? "white" : color}
color={hovered ? "white" : disabled ? "grey" : color}
handleHover={false}
/>
<span className={styles.label}>{label}</span>
</div>
) : (
<LoaderIcon color="black" size="small" />
)}
) : (
<LoaderIcon
color={hovered ? "white" : disabled ? "grey" : color}
size="small"
/>
)}
<span className={styles.label}>{label}</span>
</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;
gap: Spacings.$spacing03;
padding-block: Spacings.$spacing02;
padding-inline: Spacings.$spacing05;
padding-inline: Spacings.$spacing03;
border-radius: Radius.$big;
align-items: center;
width: 100%;
&.simple {
border: none;
padding: 0;
.text_input {
background-color: transparent;
width: 10px;
}
}
.text_input {
caret-color: Colors.$accent;
border: none;
flex: 1;
&:focus {
box-shadow: none;

View File

@ -3,10 +3,12 @@ import styles from "./TextInput.module.scss";
import { Icon } from "../Icon/Icon";
type TextInputProps = {
iconName: string;
iconName?: string;
label: string;
inputValue: string;
setInputValue: (value: string) => void;
simple?: boolean;
onSubmit?: () => void;
};
export const TextInput = ({
@ -14,17 +16,36 @@ export const TextInput = ({
label,
inputValue,
setInputValue,
simple,
onSubmit,
}: TextInputProps): JSX.Element => {
return (
<div className={styles.text_input_container}>
<div
className={`
${styles.text_input_container}
${simple ? styles.simple : ""}
`}
>
<input
className={styles.text_input}
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
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>
);
};

View File

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

View File

@ -7,5 +7,6 @@ export interface ButtonType {
color: Color;
isLoading?: boolean;
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": "用文件或网站的知识充实你的大脑"
}