mirror of
https://github.com/StanGirard/quivr.git
synced 2024-09-21 09:59:17 +03:00
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:
parent
70b2b58018
commit
cf61ce8132
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -48,7 +48,7 @@ const SelectedChatPage = (): JSX.Element => {
|
||||
onClick: () => {
|
||||
setShouldDisplayFeedCard(true);
|
||||
},
|
||||
iconName: "upload",
|
||||
iconName: "uploadFile",
|
||||
},
|
||||
{
|
||||
label: "Manage current brain",
|
||||
|
@ -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 (
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
|
||||
|
@ -50,7 +50,7 @@ const Studio = (): JSX.Element => {
|
||||
onClick: () => {
|
||||
setShouldDisplayFeedCard(true);
|
||||
},
|
||||
iconName: "upload",
|
||||
iconName: "uploadFile",
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 {
|
||||
|
49
frontend/lib/components/ui/Modal/Modal.module.scss
Normal file
49
frontend/lib/components/ui/Modal/Modal.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 />
|
@ -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 {
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
92
frontend/lib/components/ui/SingleSelector/SingleSelector.tsx
Normal file
92
frontend/lib/components/ui/SingleSelector/SingleSelector.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -7,5 +7,6 @@ export interface ButtonType {
|
||||
color: Color;
|
||||
isLoading?: boolean;
|
||||
iconName: keyof typeof iconList;
|
||||
onClick: () => void;
|
||||
onClick: () => void | Promise<void>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
@ -1 +1,4 @@
|
||||
{}
|
||||
{
|
||||
"addKnowledgeTitle": "Add Knowledge",
|
||||
"addKnowledgeSubtitle": "Feed your brain with knowledges from documents or websites"
|
||||
}
|
||||
|
@ -1 +1,4 @@
|
||||
{}
|
||||
{
|
||||
"addKnowledgeTitle": "Agregar Conocimiento",
|
||||
"addKnowledgeSubtitle": "Alimenta tu cerebro con conocimientos de documentos o sitios web"
|
||||
}
|
||||
|
@ -1 +1,4 @@
|
||||
{}
|
||||
{
|
||||
"addKnowledgeTitle": "Ajouter de la connaissance",
|
||||
"addKnowledgeSubtitle": "Nourris ton cerveau avec de la connaissance venant de documents ou de sites internets"
|
||||
}
|
||||
|
@ -1 +1,4 @@
|
||||
{}
|
||||
{
|
||||
"addKnowledgeTitle": "Adicionar Conhecimento",
|
||||
"addKnowledgeSubtitle": "Alimente seu cérebro com conhecimento de documentos ou sites da internet"
|
||||
}
|
||||
|
@ -1 +1,4 @@
|
||||
{}
|
||||
{
|
||||
"addKnowledgeTitle": "Добавить Знание",
|
||||
"addKnowledgeSubtitle": "Пополните свой мозг знаниями из документов или веб-сайтов"
|
||||
}
|
||||
|
@ -1 +1,4 @@
|
||||
{}
|
||||
{
|
||||
"addKnowledgeTitle": "添加知识",
|
||||
"addKnowledgeSubtitle": "用文件或网站的知识充实你的大脑"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user