mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-10-26 15:18:16 +03:00
feat(frontend): brain carousel (#2924)
# 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
3fb655e691
commit
54c6252b64
@ -2,6 +2,8 @@ import { EditorContent } from "@tiptap/react";
|
||||
import { useEffect } from "react";
|
||||
import "./styles.css";
|
||||
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
|
||||
import { useChatStateUpdater } from "./hooks/useChatStateUpdater";
|
||||
import { useCreateEditorState } from "./hooks/useCreateEditorState";
|
||||
import { useEditor } from "./hooks/useEditor";
|
||||
@ -20,6 +22,7 @@ export const Editor = ({
|
||||
message,
|
||||
}: EditorProps): JSX.Element => {
|
||||
const { editor } = useCreateEditorState(placeholder);
|
||||
const { currentBrain } = useBrainContext();
|
||||
|
||||
useEffect(() => {
|
||||
const htmlString = editor?.getHTML();
|
||||
@ -33,6 +36,21 @@ export const Editor = ({
|
||||
}
|
||||
}, [message, editor]);
|
||||
|
||||
useEffect(() => {
|
||||
editor?.commands.focus();
|
||||
}, [currentBrain, editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editor && placeholder) {
|
||||
(
|
||||
editor.extensionManager.extensions.find(
|
||||
(ext) => ext.name === "placeholder"
|
||||
) as { options: { placeholder: string } }
|
||||
).options.placeholder = placeholder;
|
||||
editor.view.updateState(editor.state);
|
||||
}
|
||||
}, [placeholder, editor]);
|
||||
|
||||
useChatStateUpdater({
|
||||
editor,
|
||||
setMessage,
|
||||
|
50
frontend/app/search/BrainButton/BrainButton.module.scss
Normal file
50
frontend/app/search/BrainButton/BrainButton.module.scss
Normal file
@ -0,0 +1,50 @@
|
||||
@use "styles/Radius.module.scss";
|
||||
@use "styles/Spacings.module.scss";
|
||||
@use "styles/Typography.module.scss";
|
||||
@use "styles/Variables.module.scss";
|
||||
|
||||
.brain_button_container {
|
||||
display: flex;
|
||||
padding: Spacings.$spacing03;
|
||||
border: 1px solid var(--border-0);
|
||||
border-radius: Radius.$normal;
|
||||
cursor: pointer;
|
||||
gap: Spacings.$spacing03;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: Variables.$brainButtonHeight;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-0);
|
||||
|
||||
.header {
|
||||
color: var(--primary-0);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing03;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
|
||||
.name {
|
||||
@include Typography.EllipsisOverflow;
|
||||
font-size: Typography.$small;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: Typography.$tiny;
|
||||
color: var(--text-4);
|
||||
width: 100%;
|
||||
font-style: italic;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
43
frontend/app/search/BrainButton/BrainButton.tsx
Normal file
43
frontend/app/search/BrainButton/BrainButton.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import Icon from "@/lib/components/ui/Icon/Icon";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
|
||||
|
||||
import styles from "./BrainButton.module.scss";
|
||||
|
||||
interface BrainButtonProps {
|
||||
brain: MinimalBrainForUser;
|
||||
newBrain: () => void;
|
||||
}
|
||||
|
||||
const BrainButton = ({ brain, newBrain }: BrainButtonProps): JSX.Element => {
|
||||
const { setCurrentBrainId } = useBrainContext();
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.brain_button_container}
|
||||
onClick={() => {
|
||||
setCurrentBrainId(brain.id);
|
||||
newBrain();
|
||||
}}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<Icon
|
||||
name="brain"
|
||||
size="normal"
|
||||
color={hovered ? "primary" : "black"}
|
||||
/>
|
||||
<span className={styles.name}>{brain.name}</span>
|
||||
</div>
|
||||
<span className={styles.description}>{brain.description}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrainButton;
|
@ -22,7 +22,7 @@
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: Spacings.$spacing07;
|
||||
flex-direction: column;
|
||||
|
||||
.main_wrapper {
|
||||
@ -32,10 +32,11 @@
|
||||
width: 50%;
|
||||
margin-inline: auto;
|
||||
transform: translateY(-#{Variables.$searchBarHeight});
|
||||
margin-top: 25vh;
|
||||
|
||||
@media (max-width: ScreenSizes.$small) {
|
||||
width: 100%;
|
||||
padding-inline: Spacings.$spacing07;
|
||||
padding-inline: Spacings.$spacing09;
|
||||
}
|
||||
|
||||
.quivr_logo_wrapper {
|
||||
@ -52,6 +53,34 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.brains_list_container {
|
||||
display: flex;
|
||||
margin-inline: -(Spacings.$spacing09);
|
||||
align-items: center;
|
||||
gap: calc(Spacings.$spacing05 + Spacings.$spacing01);
|
||||
|
||||
.brains_list_wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: Spacings.$spacing03;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
|
||||
@media screen and (min-width: ScreenSizes.$large) {
|
||||
grid-template-columns: repeat(3, minmax(200px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.chevron {
|
||||
visibility: visible;
|
||||
|
||||
&.disabled {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import { useBrainCreationContext } from "@/lib/components/AddBrainModal/brainCre
|
||||
import { OnboardingModal } from "@/lib/components/OnboardingModal/OnboardingModal";
|
||||
import { PageHeader } from "@/lib/components/PageHeader/PageHeader";
|
||||
import { UploadDocumentModal } from "@/lib/components/UploadDocumentModal/UploadDocumentModal";
|
||||
import Icon from "@/lib/components/ui/Icon/Icon";
|
||||
import { MessageInfoBox } from "@/lib/components/ui/MessageInfoBox/MessageInfoBox";
|
||||
import { QuivrButton } from "@/lib/components/ui/QuivrButton/QuivrButton";
|
||||
import { SearchBar } from "@/lib/components/ui/SearchBar/SearchBar";
|
||||
@ -19,10 +20,16 @@ import { useUserData } from "@/lib/hooks/useUserData";
|
||||
import { redirectToLogin } from "@/lib/router/redirectToLogin";
|
||||
import { ButtonType } from "@/lib/types/QuivrButton";
|
||||
|
||||
import BrainButton from "./BrainButton/BrainButton";
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
const Search = (): JSX.Element => {
|
||||
const [isUserDataFetched, setIsUserDataFetched] = useState(false);
|
||||
const [isNewBrain, setIsNewBrain] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [transitionDirection, setTransitionDirection] = useState("");
|
||||
const brainsPerPage = 6;
|
||||
|
||||
const pathname = usePathname();
|
||||
const { session } = useSupabase();
|
||||
const { isBrainCreationModalOpened, setIsBrainCreationModalOpened } =
|
||||
@ -45,6 +52,13 @@ const Search = (): JSX.Element => {
|
||||
},
|
||||
]);
|
||||
|
||||
const newBrain = () => {
|
||||
setIsNewBrain(true);
|
||||
setTimeout(() => {
|
||||
setIsNewBrain(false);
|
||||
}, 750);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (userIdentityData) {
|
||||
setIsUserDataFetched(true);
|
||||
@ -74,6 +88,27 @@ const Search = (): JSX.Element => {
|
||||
}
|
||||
}, [pathname, session]);
|
||||
|
||||
const totalPages = Math.ceil(allBrains.length / brainsPerPage);
|
||||
|
||||
const handleNextPage = () => {
|
||||
if (currentPage < totalPages - 1) {
|
||||
setTransitionDirection("next");
|
||||
setCurrentPage((prevPage) => prevPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePreviousPage = () => {
|
||||
if (currentPage > 0) {
|
||||
setTransitionDirection("prev");
|
||||
setCurrentPage((prevPage) => prevPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const displayedBrains = allBrains.slice(
|
||||
currentPage * brainsPerPage,
|
||||
(currentPage + 1) * brainsPerPage
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.main_container}>
|
||||
@ -90,7 +125,46 @@ const Search = (): JSX.Element => {
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.search_bar_wrapper}>
|
||||
<SearchBar />
|
||||
<SearchBar newBrain={isNewBrain} />
|
||||
</div>
|
||||
<div className={styles.brains_list_container}>
|
||||
<div
|
||||
className={`${styles.chevron} ${
|
||||
currentPage === 0 ? styles.disabled : ""
|
||||
}`}
|
||||
onClick={handlePreviousPage}
|
||||
>
|
||||
<Icon
|
||||
name="chevronLeft"
|
||||
size="big"
|
||||
color="black"
|
||||
handleHover={true}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.brains_list_wrapper} ${
|
||||
transitionDirection === "next"
|
||||
? styles.slide_next
|
||||
: styles.slide_prev
|
||||
}`}
|
||||
>
|
||||
{displayedBrains.map((brain, index) => (
|
||||
<BrainButton key={index} brain={brain} newBrain={newBrain} />
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.chevron} ${
|
||||
currentPage >= totalPages - 1 ? styles.disabled : ""
|
||||
}`}
|
||||
onClick={handleNextPage}
|
||||
>
|
||||
<Icon
|
||||
name="chevronRight"
|
||||
size="big"
|
||||
color="black"
|
||||
handleHover={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -46,6 +46,10 @@
|
||||
.brain_name {
|
||||
@include textColor(text-3);
|
||||
@include Typography.EllipsisOverflow;
|
||||
|
||||
&.new {
|
||||
color: var(--primary-0);
|
||||
}
|
||||
}
|
||||
|
||||
.warning {
|
||||
@ -66,14 +70,6 @@
|
||||
@extend %header_style;
|
||||
}
|
||||
|
||||
.no_brain_selected {
|
||||
@include textColor(warning);
|
||||
|
||||
.strong {
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
.no_credits_left {
|
||||
@include textColor(dangerous);
|
||||
}
|
||||
|
@ -9,11 +9,13 @@ import { LoaderIcon } from "../ui/LoaderIcon/LoaderIcon";
|
||||
interface CurrentBrainProps {
|
||||
allowingRemoveBrain: boolean;
|
||||
remainingCredits: number | null;
|
||||
isNewBrain?: boolean;
|
||||
}
|
||||
|
||||
export const CurrentBrain = ({
|
||||
allowingRemoveBrain,
|
||||
remainingCredits,
|
||||
isNewBrain,
|
||||
}: CurrentBrainProps): JSX.Element => {
|
||||
const { currentBrain, setCurrentBrainId } = useBrainContext();
|
||||
const removeCurrentBrain = (): void => {
|
||||
@ -32,13 +34,7 @@ export const CurrentBrain = ({
|
||||
}
|
||||
|
||||
if (!currentBrain) {
|
||||
return (
|
||||
<div className={styles.no_brain_selected}>
|
||||
<span>
|
||||
Press <strong className={styles.strong}>@</strong> to select a Brain
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -47,8 +43,16 @@ export const CurrentBrain = ({
|
||||
<div className={styles.left}>
|
||||
<span className={styles.title}>Talking to</span>
|
||||
<div className={styles.brain_name_wrapper}>
|
||||
<Icon name="brain" size="small" color="black" />
|
||||
<span className={styles.brain_name}>{currentBrain.name}</span>
|
||||
<Icon
|
||||
name="brain"
|
||||
size="small"
|
||||
color={isNewBrain ? "primary" : "black"}
|
||||
/>
|
||||
<span
|
||||
className={`${styles.brain_name} ${isNewBrain ? styles.new : ""}`}
|
||||
>
|
||||
{currentBrain.name}
|
||||
</span>
|
||||
{bulkNotifications.some(
|
||||
(bulkNotif) =>
|
||||
bulkNotif.brain_id === currentBrain.id &&
|
||||
|
@ -13,16 +13,24 @@
|
||||
overflow: hidden;
|
||||
box-shadow: BoxShadow.$large;
|
||||
|
||||
&.new_brain {
|
||||
border-color: var(--primary-0);
|
||||
}
|
||||
|
||||
.editor_wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: Spacings.$spacing05;
|
||||
padding-top: 0;
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.3;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
&.current {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.search_icon {
|
||||
|
@ -15,11 +15,14 @@ import { LoaderIcon } from "../LoaderIcon/LoaderIcon";
|
||||
|
||||
export const SearchBar = ({
|
||||
onSearch,
|
||||
newBrain,
|
||||
}: {
|
||||
onSearch?: () => void;
|
||||
newBrain?: boolean;
|
||||
}): JSX.Element => {
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [isDisabled, setIsDisabled] = useState(true);
|
||||
const [placeholder, setPlaceholder] = useState("Select a @brain");
|
||||
const { message, setMessage } = useChatInput();
|
||||
const { setMessages } = useChatContext();
|
||||
const { addQuestion } = useChat();
|
||||
@ -34,6 +37,10 @@ export const SearchBar = ({
|
||||
setIsDisabled(message === "");
|
||||
}, [message]);
|
||||
|
||||
useEffect(() => {
|
||||
setPlaceholder(currentBrain ? "Ask a question..." : "Select a @brain");
|
||||
}, [currentBrain]);
|
||||
|
||||
const submit = async (): Promise<void> => {
|
||||
if (!!remainingCredits && !!currentBrain && !searching) {
|
||||
setSearching(true);
|
||||
@ -52,21 +59,26 @@ export const SearchBar = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.search_bar_wrapper}>
|
||||
<div
|
||||
className={`${styles.search_bar_wrapper} ${
|
||||
newBrain ? styles.new_brain : ""
|
||||
}`}
|
||||
>
|
||||
<CurrentBrain
|
||||
allowingRemoveBrain={true}
|
||||
remainingCredits={remainingCredits}
|
||||
isNewBrain={newBrain}
|
||||
/>
|
||||
<div
|
||||
className={`${styles.editor_wrapper} ${
|
||||
!remainingCredits ? styles.disabled : ""
|
||||
}`}
|
||||
} ${currentBrain ? styles.current : ""}`}
|
||||
>
|
||||
<Editor
|
||||
message={message}
|
||||
setMessage={setMessage}
|
||||
onSubmit={() => void submit()}
|
||||
placeholder="Ask a question..."
|
||||
placeholder={placeholder}
|
||||
></Editor>
|
||||
{searching ? (
|
||||
<LoaderIcon size="big" color="accent" />
|
||||
|
@ -1 +1,2 @@
|
||||
$small: 768px
|
||||
$small: 768px;
|
||||
$large: 1080px;
|
||||
|
@ -1,3 +1,4 @@
|
||||
$searchBarHeight: 62px;
|
||||
$pageHeaderHeight: 48px;
|
||||
$menuWidth: 230px;
|
||||
$brainButtonHeight: 80px;
|
||||
|
Loading…
Reference in New Issue
Block a user