feat(frontend): search modal - remove parameters and explore buttons (#2094)

# 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-01-26 16:06:01 -08:00 committed by GitHub
parent 7b2ff469eb
commit 2baa405991
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 258 additions and 543 deletions

View File

@ -8,10 +8,12 @@ import { PropsWithChildren, useEffect } from "react";
import { Menu } from "@/lib/components/Menu/Menu";
import { useOutsideClickListener } from "@/lib/components/Menu/hooks/useOutsideClickListener";
import { NotificationBanner } from "@/lib/components/NotificationBanner";
import SearchModal from "@/lib/components/SearchModal/SearchModal";
import { BrainProvider, ChatProvider } from "@/lib/context";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { ChatsProvider } from "@/lib/context/ChatsProvider";
import { MenuProvider } from "@/lib/context/MenuProvider/Menu-provider";
import { SearchModalProvider } from "@/lib/context/SearchModalProvider/search-modal-provider";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { UpdateMetadata } from "@/lib/helpers/updateMetadata";
import { usePageTracking } from "@/services/analytics/june/usePageTracking";
@ -30,7 +32,8 @@ if (
// This wrapper is used to make effect calls at a high level in app rendering.
const App = ({ children }: PropsWithChildren): JSX.Element => {
const { fetchAllBrains, fetchDefaultBrain, fetchPublicPrompts } = useBrainContext();
const { fetchAllBrains, fetchDefaultBrain, fetchPublicPrompts } =
useBrainContext();
const { onClickOutside } = useOutsideClickListener();
const { session } = useSupabase();
@ -38,25 +41,28 @@ const App = ({ children }: PropsWithChildren): JSX.Element => {
useEffect(() => {
if (session?.user) {
void fetchAllBrains();
void fetchDefaultBrain();
void fetchPublicPrompts();
posthog.identify(session.user.id, { email: session.user.email });
posthog.startSessionRecording();
void fetchAllBrains();
void fetchDefaultBrain();
void fetchPublicPrompts();
posthog.identify(session.user.id, { email: session.user.email });
posthog.startSessionRecording();
}
}, [session]);
return (
<PostHogProvider client={posthog}>
<div className="flex flex-1 flex-col overflow-auto">
<NotificationBanner />
<div className="relative h-full w-full flex justify-stretch items-stretch overflow-auto">
<SearchModalProvider>
<SearchModal />
<NotificationBanner />
<div className="relative h-full w-full flex justify-stretch items-stretch overflow-auto">
<Menu />
<div onClick={onClickOutside} className="flex-1">
{children}
{children}
</div>
<UpdateMetadata />
</div>
</div>
</SearchModalProvider>
</div>
</PostHogProvider>
);

View File

@ -19,6 +19,7 @@ export const useCreateEditorState = (placeholder?: string) => {
addKeyboardShortcuts: () => {
return {
Enter: () => true,
Escape: () => true,
};
},
});

View File

@ -32,7 +32,7 @@ const RelatedBrains = ({ closeBrains }: RelatedBrainsProps): JSX.Element => {
const g = Math.round(lerp(211, 43, t));
const b = Math.round(lerp(211, 226, t));
const isCurrentBrain =
brain.id === messages[messages.length - 1].brain_id;
brain.id === messages[messages.length - 1]?.brain_id;
return { color: `rgb(${r}, ${g}, ${b})`, isCurrentBrain: isCurrentBrain };
});

View File

@ -9,6 +9,7 @@ import { CHATS_DATA_KEY } from "@/lib/api/chat/config";
import { useChatApi } from "@/lib/api/chat/useChatApi";
import { useChatContext } from "@/lib/context";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useSearchModalContext } from "@/lib/context/SearchModalProvider/hooks/useSearchModalContext";
import { getChatNameFromQuestion } from "@/lib/helpers/getChatNameFromQuestion";
import { useToast } from "@/lib/hooks";
import { useOnboarding } from "@/lib/hooks/useOnboarding";
@ -40,6 +41,7 @@ export const useChat = () => {
const {
chatConfig: { model, maxTokens, temperature },
} = useLocalStorageChatConfig();
const { isVisible } = useSearchModalContext();
const { addStreamQuestion } = useQuestion();
const { t } = useTranslation(["chat"]);
@ -60,7 +62,7 @@ export const useChat = () => {
let currentChatId = chatId;
//if chatId is not set, create a new chat. Chat name is from the first question
if (currentChatId === undefined) {
if (currentChatId === undefined || isVisible) {
const chat = await createChat(getChatNameFromQuestion(question));
currentChatId = chat.chat_id;
setChatId(currentChatId);

View File

@ -18,8 +18,7 @@ export const useHandleStream = () => {
const { done, value } = await reader.read();
if (done) {
if (incompleteData !== "") {
if (incompleteData !== "") {
// Try to parse any remaining incomplete data
try {
@ -41,12 +40,7 @@ export const useHandleStream = () => {
// Concatenate incomplete data with new chunk
const rawData = incompleteData + decoder.decode(value, { stream: true });
console.log("Raw data before cleaning:", rawData);
const dataStrings = rawData
.trim()
.split("data: ")
.filter(Boolean);
const dataStrings = rawData.trim().split("data: ").filter(Boolean);
dataStrings.forEach((data, index, array) => {
if (index === array.length - 1 && !data.endsWith("\n")) {
@ -74,4 +68,3 @@ export const useHandleStream = () => {
handleStream,
};
};

View File

@ -1,125 +0,0 @@
import { useTranslation } from "react-i18next";
import { CgFileDocument } from "react-icons/cg";
import { LuPlusCircle } from "react-icons/lu";
import { MdLink } from "react-icons/md";
import Button from "@/lib/components/ui/Button";
import { Modal } from "@/lib/components/ui/Modal";
import { PublicBrain } from "@/lib/context/BrainProvider/types";
import { getBrainIconFromBrainType } from "@/lib/helpers/getBrainIconFromBrainType";
import { SecretsDefinitionFields } from "./components/SecretsDefinitionFields";
import { usePublicBrainItem } from "./hooks/usePublicBrainItem";
import { formatDate } from "./utils/formatDate";
type PublicBrainItemProps = {
brain: PublicBrain;
};
export const PublicBrainItem = ({
brain,
}: PublicBrainItemProps): JSX.Element => {
const {
isUserSubscribedToBrain,
subscriptionRequestPending,
isSubscriptionModalOpened,
setIsSubscriptionModalOpened,
handleCopyBrainLink,
handleBrainSubscription,
register,
} = usePublicBrainItem({
brain,
});
const { t } = useTranslation("brain");
const subscribeButton = (
<Button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
void handleBrainSubscription();
}}
disabled={isUserSubscribedToBrain || subscriptionRequestPending}
isLoading={subscriptionRequestPending}
className="bg-secondary text-primary rounded-lg border-0 w-content mt-3 disabled:bg-secondary hover:bg-primary/50 disabled:hover:bg-primary/50 w-full text-md"
>
{!isUserSubscribedToBrain && <LuPlusCircle className="text-md" />}
{isUserSubscribedToBrain
? t("public_brain_already_subscribed_button_label")
: t("public_brain_subscribe_button_label")}
</Button>
);
const isBrainDescriptionEmpty = brain.description === "";
const brainDescription = isBrainDescriptionEmpty
? t("empty_brain_description")
: brain.description;
return (
<Modal
isOpen={isSubscriptionModalOpened}
setOpen={setIsSubscriptionModalOpened}
CloseTrigger={<div />}
Trigger={
<div className="flex justify-center items-center flex-col flex-1 w-full h-full shadow-md dark:shadow-primary/25 hover:shadow-xl transition-shadow rounded-xl overflow-hidden bg-white dark:bg-black border border-black/10 dark:border-white/25 cursor-pointer pb-2">
<div className="w-full">
<div className="w-full py-2 flex gap-2 justify-center items-center bg-primary bg-opacity-40 px-2">
{getBrainIconFromBrainType(brain.brain_type, {
iconSize: 24,
DocBrainIcon: CgFileDocument,
iconClassName: "text-primary",
})}
<span className="line-clamp-1 mr-2 font-semibold text-md">
{brain.name}
</span>
</div>
</div>
<div className="flex-1 py-2">
<p
className={`line-clamp-2 text-center px-5 ${
isBrainDescriptionEmpty && "text-gray-400"
}`}
>
{brainDescription}
</p>
</div>
<div className="w-full px-2">{subscribeButton}</div>
</div>
}
>
<div>
<p className="text-2xl font-medium text-center mb-10 flex items-center justify-center">
<span className="mr-2">
{getBrainIconFromBrainType(brain.brain_type, {
iconSize: 30,
DocBrainIcon: CgFileDocument,
})}
</span>
{brain.name}
</p>
<p className={`mb-10 ${isBrainDescriptionEmpty && "text-gray-400"}`}>
{brainDescription}
</p>
<p className="mb-5">
<span>
<span className="mr-2">{t("public_brain_last_update_label")}:</span>
{formatDate(brain.last_update)}
</span>
</p>
<SecretsDefinitionFields
register={register}
secrets={brain.brain_definition?.secrets}
/>
<div className="flex flex-1 justify-between items-center">
<Button
onClick={() => void handleCopyBrainLink()}
className="p-1 bg-white border-solid border border-gray-300 rounded-md hover:bg-gray-100"
>
<MdLink size="20" color="gray" />
</Button>
<div>{subscribeButton}</div>
</div>
</div>
</Modal>
);
};

View File

@ -1,43 +0,0 @@
import { Fragment } from "react";
import { UseFormRegister } from "react-hook-form";
import { ApiBrainDefinitionSecret } from "@/lib/api/brain/types";
type SecretsDefinitionFieldsProps = {
secrets?: ApiBrainDefinitionSecret[];
register: UseFormRegister<{
secrets?: Record<string, string>;
}>;
};
export const SecretsDefinitionFields = ({
secrets,
register,
}: SecretsDefinitionFieldsProps): JSX.Element => {
if (secrets === undefined) {
return <Fragment />;
}
return (
<div className="mb-3">
{secrets.map((secret) => (
<div key={secret.name} className="flex flex-col">
<div className="flex flex-col">
<p className="text-sm font-bold mb-1">{secret.name}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{secret.description}
</p>
</div>
<div className="flex flex-col">
<input
type="text"
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
placeholder={secret.description}
{...register(`secrets.${secret.name}`)}
/>
</div>
</div>
))}
</div>
);
};

View File

@ -1,110 +0,0 @@
import { useSearchParams } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useSubscriptionApi } from "@/lib/api/subscription/useSubscriptionApi";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { PublicBrain } from "@/lib/context/BrainProvider/types";
import { getAxiosErrorParams } from "@/lib/helpers/getAxiosErrorParams";
import { useToast } from "@/lib/hooks";
import { generatePublicBrainLink } from "../utils/generatePublicBrainLink";
type UseSubscribeToBrainProps = {
brain: PublicBrain;
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const usePublicBrainItem = ({ brain }: UseSubscribeToBrainProps) => {
const brainId = brain.id;
const { subscribeToBrain } = useSubscriptionApi();
const [subscriptionRequestPending, setSubscriptionRequestPending] =
useState(false);
const { publish } = useToast();
const { allBrains, fetchAllBrains } = useBrainContext();
const { register, watch } = useForm<{
secrets?: Record<string, string>;
}>({});
const secrets = watch("secrets") ?? {};
const searchParams = useSearchParams();
const urlBrainId = searchParams?.get("brainId");
const [isSubscriptionModalOpened, setIsSubscriptionModalOpened] = useState(
urlBrainId === brainId
);
const isUserSubscribedToBrain =
allBrains.find((_brain) => _brain.id === brainId) !== undefined;
const { t } = useTranslation("brain");
const subscribeUserToBrain = async () => {
try {
setSubscriptionRequestPending(true);
await subscribeToBrain(brainId, secrets);
setIsSubscriptionModalOpened(false);
await fetchAllBrains();
publish({
text: t("public_brain_subscription_success_message"),
variant: "success",
});
} catch (e) {
const error = getAxiosErrorParams(e);
if (error !== undefined) {
publish({
text: error.message,
variant: "danger",
});
return;
}
publish({
text: JSON.stringify(error),
variant: "danger",
});
} finally {
setSubscriptionRequestPending(false);
}
};
const handleCopyBrainLink = async () => {
await navigator.clipboard.writeText(generatePublicBrainLink(brainId));
publish({
variant: "success",
text: t("copiedToClipboard", { ns: "brain" }),
});
};
const handleBrainSubscription = () => {
const brainHasSecretsLength = brain.brain_definition?.secrets?.length ?? 0;
if (brain.brain_type === "api") {
const filledSecretsLength = Object.keys(secrets).filter(
(key) => secrets[key].length > 0
).length;
if (
brainHasSecretsLength > 0 &&
filledSecretsLength !== brainHasSecretsLength
) {
setIsSubscriptionModalOpened(true);
return;
}
}
void subscribeUserToBrain();
};
return {
subscriptionRequestPending,
isUserSubscribedToBrain,
setIsSubscriptionModalOpened,
isSubscriptionModalOpened,
handleCopyBrainLink,
handleBrainSubscription,
register,
};
};

View File

@ -1 +0,0 @@
export * from "./PublicBrainItem";

View File

@ -1,2 +0,0 @@
export const formatDate = (date: string): string =>
new Date(date).toLocaleDateString();

View File

@ -1,2 +0,0 @@
export const generatePublicBrainLink = (brainId: string): string =>
`${window.location.origin}/brains-management/library?brainId=${brainId}`;

View File

@ -1 +0,0 @@
export * from "./PublicBrainItem";

View File

@ -1,44 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { PUBLIC_BRAINS_KEY } from "@/lib/api/brain/config";
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
import { PublicBrain } from "@/lib/context/BrainProvider/types";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useBrainsLibrary = () => {
const [searchBarText, setSearchBarText] = useState("");
const { getPublicBrains } = useBrainApi();
const { data: publicBrains = [], isLoading } = useQuery({
queryKey: [PUBLIC_BRAINS_KEY],
queryFn: getPublicBrains,
});
const [displayingPublicBrains, setDisplayingPublicBrains] = useState<
PublicBrain[]
>([]);
useEffect(() => {
setDisplayingPublicBrains(publicBrains);
}, [publicBrains]);
useEffect(() => {
if (searchBarText === "") {
setDisplayingPublicBrains(publicBrains);
return;
}
setDisplayingPublicBrains(
publicBrains.filter((brain) =>
brain.name.toLowerCase().includes(searchBarText.toLowerCase())
)
);
}, [publicBrains, searchBarText]);
return {
displayingPublicBrains,
searchBarText,
setSearchBarText,
isLoading,
};
};

View File

@ -1,25 +0,0 @@
"use client";
import { ReactNode } from "react";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { redirectToLogin } from "@/lib/router/redirectToLogin";
interface LayoutProps {
children?: ReactNode;
}
const Layout = ({ children }: LayoutProps): JSX.Element => {
const { session } = useSupabase();
if (session === null) {
redirectToLogin();
}
return (
<div className="relative h-full w-full flex justify-stretch items-stretch overflow-auto">
{children}
</div>
);
};
export default Layout;

View File

@ -1,62 +0,0 @@
"use client";
import { useTranslation } from "react-i18next";
import { LuGlobe, LuSearch } from "react-icons/lu";
import Field from "@/lib/components/ui/Field";
import Spinner from "@/lib/components/ui/Spinner";
import { PublicBrainItem } from "./components/PublicBrainItem/PublicBrainItem";
import { useBrainsLibrary } from "./hooks/useBrainsLibrary";
const BrainsLibrary = (): JSX.Element => {
const { displayingPublicBrains, searchBarText, setSearchBarText, isLoading } =
useBrainsLibrary();
const { t } = useTranslation(["brain", "translation"]);
return (
<div className="w-full p-10 px-4 md:px-10 overflow-auto">
<div className="flex flex-row items-center justify-center gap-2 w-full">
<LuGlobe className="text-primary" size={30} />
<span className="font-semibold text-3xl">
{t("translation:Explore")}
</span>
</div>
<div className="flex justify-center md:justify-end w-full mt-3">
<div>
<Field
value={searchBarText}
onChange={(e) => setSearchBarText(e.target.value)}
name="search"
inputClassName="w-max lg:min-w-[300px] md:min-w-[200px] min-w-[100px] rounded-3xl bg-white border-none"
placeholder={t("brain:public_brains_search_bar_placeholder")}
icon={<LuSearch className="text-primary" size={20} />}
/>
</div>
</div>
<div className="flex flex-row items-center justify-center gap-2 w-full mt-3">
<p className="font-normal text-2xl text-center">
{t("brain:explore_brains")}
</p>
<div className="flex"></div>
</div>
<div className="flex flex-1 flex-col items-center justify-center">
{isLoading && (
<div className="flex justify-center items-center flex-1">
<Spinner className="text-4xl" />
</div>
)}
<div className="w-full lg:grid-cols-4 md:grid-cols-3 grid mt-5 gap-3 items-stretch">
{displayingPublicBrains.map((brain) => (
<div key={brain.id} className="h-[180px]">
<PublicBrainItem brain={brain} />
</div>
))}
</div>
</div>
</div>
);
};
export default BrainsLibrary;

View File

@ -1,16 +1,16 @@
@use '@/styles/Colors.module.scss';
@use '@/styles/Spacings.module.scss';
@use '@/styles/ZIndex.module.scss';
@use "@/styles/Colors.module.scss";
@use "@/styles/Spacings.module.scss";
@use "@/styles/ZIndexes.module.scss";
.menu_control_button_wrapper {
background-color: transparent;
position: absolute;
top: Spacings.$spacing05;
left: Spacings.$spacing05;
transition: margin-left 0.2s ease-in-out;
z-index: ZIndex.$overlay;
background-color: transparent;
position: absolute;
top: Spacings.$spacing05;
left: Spacings.$spacing05;
transition: margin-left 0.2s ease-in-out;
z-index: ZIndexes.$overlay;
&.shifted {
margin-left: 260px;
}
}
&.shifted {
margin-left: 260px;
}
}

View File

@ -8,10 +8,8 @@ import { useMenuContext } from "@/lib/context/MenuProvider/hooks/useMenuContext"
import styles from "./Menu.module.scss";
import { AnimatedDiv } from "./components/AnimationDiv";
import { BrainsManagementButton } from "./components/BrainsManagementButton";
import { DiscussionButton } from "./components/DiscussionButton";
import { ExplorerButton } from "./components/ExplorerButton";
import { DiscussionButton } from "./components/DiscussionButton/DiscussionButton";
import { MenuHeader } from "./components/MenuHeader";
import { ParametersButton } from "./components/ParametersButton";
import { ProfileButton } from "./components/ProfileButton";
import { UpgradeToPlus } from "./components/UpgradeToPlus";
@ -49,9 +47,7 @@ export const Menu = (): JSX.Element => {
<div className="flex flex-1 w-full">
<div className="w-full gap-2 flex flex-col">
<DiscussionButton />
<ExplorerButton />
<BrainsManagementButton />
<ParametersButton />
</div>
</div>
<div>

View File

@ -1,27 +0,0 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useTranslation } from "react-i18next";
import { LuChevronRight, LuSearch } from "react-icons/lu";
import { MenuButton } from "@/lib/components/Menu/components/MenuButton";
import { cn } from "@/lib/utils";
export const DiscussionButton = (): JSX.Element => {
const pathname = usePathname() ?? "";
const isSelected = pathname.includes("/search");
const { t } = useTranslation("chat");
return (
<Link href="/search">
<MenuButton
label={t("search")}
startIcon={<LuSearch />}
endIcon={<LuChevronRight size={18} />}
className={cn(
"w-full hover:bg-secondary py-3",
isSelected ? "bg-secondary" : ""
)}
/>
</Link>
);
};

View File

@ -0,0 +1,44 @@
@use "@/styles/Colors.module.scss";
@use "@/styles/IconSizes.module.scss";
@use "@/styles/Spacings.module.scss";
.button_wrapper {
display: flex;
padding: Spacings.$spacing04;
justify-content: space-between;
align-items: center;
border: 1px solid Colors.$light-grey;
border-radius: 12px;
background-color: Colors.$white;
cursor: pointer;
color: Colors.$dark-grey;
transition: box-shadow 0.3s ease;
.left_wrapper {
display: flex;
align-items: center;
gap: Spacings.$spacing03;
.shortcuts_wrapper {
display: flex;
align-items: center;
gap: Spacings.$spacing01;
.shortcut {
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
border: 1px solid Colors.$light-grey;
border-radius: 3px;
width: 16px;
height: 16px;
}
}
}
&:hover {
border-color: Colors.$accent;
box-shadow: 0 0 0 2px Colors.$accent;
}
}

View File

@ -0,0 +1,34 @@
import { useState } from "react";
import Icon from "@/lib/components/ui/Icon/Icon";
import { useSearchModalContext } from "@/lib/context/SearchModalProvider/hooks/useSearchModalContext";
import styles from "./DiscussionButton.module.scss";
export const DiscussionButton = (): JSX.Element => {
const [hovered, setHovered] = useState(false);
const { setIsVisible } = useSearchModalContext();
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
setIsVisible(true);
};
return (
<div
className={styles.button_wrapper}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onClick={handleClick}
>
<div className={styles.left_wrapper}>
<span>New Search</span>
<div className={styles.shortcuts_wrapper}>
<div className={styles.shortcut}></div>
<div className={styles.shortcut}>K</div>
</div>
</div>
<Icon name="search" size="normal" color={hovered ? "accent" : "black"} />
</div>
);
};

View File

@ -1,27 +0,0 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useTranslation } from "react-i18next";
import { LuChevronRight, LuGlobe } from "react-icons/lu";
import { MenuButton } from "@/lib/components/Menu/components/MenuButton";
import { cn } from "@/lib/utils";
export const ExplorerButton = (): JSX.Element => {
const pathname = usePathname() ?? "";
const isSelected = pathname.includes("/library");
const { t } = useTranslation("brain");
return (
<Link href={`/brains-management/library`}>
<MenuButton
label={t("brain_library_button_label")}
startIcon={<LuGlobe />}
endIcon={<LuChevronRight size={18} />}
className={cn(
"w-full hover:bg-secondary py-3",
isSelected ? "bg-secondary" : ""
)}
/>
</Link>
);
};

View File

@ -1,27 +0,0 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useTranslation } from "react-i18next";
import { LuChevronRight, LuSettings } from "react-icons/lu";
import { MenuButton } from "@/lib/components/Menu/components/MenuButton";
import { cn } from "@/lib/utils";
export const ParametersButton = (): JSX.Element => {
const pathname = usePathname() ?? "";
const isSelected = pathname.includes("/user");
const { t } = useTranslation("chat");
return (
<Link href="/user">
<MenuButton
label={t("parameters")}
startIcon={<LuSettings />}
endIcon={<LuChevronRight size={18} />}
className={cn(
"w-full hover:bg-secondary py-3",
isSelected ? "bg-secondary" : ""
)}
/>
</Link>
);
};

View File

@ -0,0 +1,25 @@
@use "@/styles/Colors.module.scss";
@use "@/styles/ScreenSizes.module.scss";
@use "@/styles/Spacings.module.scss";
@use "@/styles/ZIndexes.module.scss";
.search_modal_wrapper {
display: flex;
background-color: Colors.$dark-black;
height: 100%;
width: 100%;
position: absolute;
z-index: ZIndexes.$modal;
opacity: 0.94;
align-items: center;
justify-content: center;
.search_bar_wrapper {
width: 50%;
@media (max-width: ScreenSizes.$small) {
width: 100%;
margin-inline: Spacings.$spacing07;
}
}
}

View File

@ -0,0 +1,68 @@
import { useEffect, useRef } from "react";
import { useSearchModalContext } from "@/lib/context/SearchModalProvider/hooks/useSearchModalContext";
import styles from "./SearchModal.module.scss";
import { SearchBar } from "../ui/SearchBar/SearchBar";
export const SearchModal = (): JSX.Element => {
const { isVisible, setIsVisible } = useSearchModalContext();
const searchBarRef = useRef(null);
useEffect(() => {
console.log(`isVisible has changed to: ${isVisible}`);
}, [isVisible]);
const keydownHandler = ({
key,
metaKey,
}: {
key: string;
metaKey: boolean;
}) => {
if (metaKey && key === "k") {
setIsVisible(true);
} else if (key === "Escape") {
setIsVisible(false);
}
};
const mousedownHandler = (event: MouseEvent) => {
if (
!(searchBarRef.current as HTMLElement | null)?.contains(
event.target as Node
)
) {
setIsVisible(false);
}
};
const handleSearch = () => {
setIsVisible(false);
};
useEffect(() => {
document.addEventListener("keydown", keydownHandler);
window.addEventListener("click", mousedownHandler);
return () => {
document.removeEventListener("keydown", keydownHandler);
window.removeEventListener("click", mousedownHandler);
};
}, []);
if (!isVisible) {
return <></>;
}
return (
<div className={styles.search_modal_wrapper}>
<div className={styles.search_bar_wrapper} ref={searchBarRef}>
<SearchBar onSearch={handleSearch} />
</div>
</div>
);
};
export default SearchModal;

View File

@ -11,7 +11,11 @@ import styles from "./SearchBar.module.scss";
import { LoaderIcon } from "../LoaderIcon/LoaderIcon";
export const SearchBar = (): JSX.Element => {
export const SearchBar = ({
onSearch,
}: {
onSearch?: () => void;
}): JSX.Element => {
const [searching, setSearching] = useState(false);
const [isDisabled, setIsDisabled] = useState(true);
const { message, setMessage } = useChatInput();
@ -32,6 +36,9 @@ export const SearchBar = (): JSX.Element => {
setSearching(true);
setMessages([]);
try {
if (onSearch) {
onSearch();
}
await addQuestion(message);
} catch (error) {
console.error(error);

View File

@ -0,0 +1,13 @@
import { useContext } from "react";
import { SearchModalContext } from "../search-modal-provider";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useSearchModalContext = () => {
const context = useContext(SearchModalContext);
if (context === undefined) {
throw new Error("useSearchModalContext must be used within a MenuProvider");
}
return context;
};

View File

@ -0,0 +1,24 @@
import { createContext, useState } from "react";
type SearchModalContextType = {
isVisible: boolean;
setIsVisible: React.Dispatch<React.SetStateAction<boolean>>;
};
export const SearchModalContext = createContext<
SearchModalContextType | undefined
>(undefined);
export const SearchModalProvider = ({
children,
}: {
children: React.ReactNode;
}): JSX.Element => {
const [isVisible, setIsVisible] = useState(false);
return (
<SearchModalContext.Provider value={{ isVisible, setIsVisible }}>
{children}
</SearchModalContext.Provider>
);
};

View File

@ -2,7 +2,9 @@ $white: #ffffff;
$lightest-grey: #f5f5f5;
$light-grey: #d3d3d3;
$normal-grey: #c8c8c8;
$dark-grey: #707070;
$black: #11243e;
$dark-black: #081621;
$primary: #6142d4;
$secondary: #f3ecff;
$tertiary: #f6f4ff;

View File

@ -1,7 +0,0 @@
$base: 1000;
$overlay: 1010;
$modal: 1020;
$navbar: 1030;
$tooltip: 1040;
$refresh-banner: 1050;
$sentry-modal: 1060;

View File

@ -0,0 +1,3 @@
$base: 1000;
$overlay: 1010;
$modal: 1020;