mirror of
https://github.com/StanGirard/quivr.git
synced 2024-09-11 13:15:41 +03:00
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:
parent
7b2ff469eb
commit
2baa405991
@ -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>
|
||||
);
|
||||
|
@ -19,6 +19,7 @@ export const useCreateEditorState = (placeholder?: string) => {
|
||||
addKeyboardShortcuts: () => {
|
||||
return {
|
||||
Enter: () => true,
|
||||
Escape: () => true,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -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 };
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -1 +0,0 @@
|
||||
export * from "./PublicBrainItem";
|
@ -1,2 +0,0 @@
|
||||
export const formatDate = (date: string): string =>
|
||||
new Date(date).toLocaleDateString();
|
@ -1,2 +0,0 @@
|
||||
export const generatePublicBrainLink = (brainId: string): string =>
|
||||
`${window.location.origin}/brains-management/library?brainId=${brainId}`;
|
@ -1 +0,0 @@
|
||||
export * from "./PublicBrainItem";
|
@ -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,
|
||||
};
|
||||
};
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
25
frontend/lib/components/SearchModal/SearchModal.module.scss
Normal file
25
frontend/lib/components/SearchModal/SearchModal.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
68
frontend/lib/components/SearchModal/SearchModal.tsx
Normal file
68
frontend/lib/components/SearchModal/SearchModal.tsx
Normal 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;
|
@ -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);
|
||||
|
@ -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;
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
||||
|
@ -1,7 +0,0 @@
|
||||
$base: 1000;
|
||||
$overlay: 1010;
|
||||
$modal: 1020;
|
||||
$navbar: 1030;
|
||||
$tooltip: 1040;
|
||||
$refresh-banner: 1050;
|
||||
$sentry-modal: 1060;
|
3
frontend/styles/_ZIndexes.module.scss
Normal file
3
frontend/styles/_ZIndexes.module.scss
Normal file
@ -0,0 +1,3 @@
|
||||
$base: 1000;
|
||||
$overlay: 1010;
|
||||
$modal: 1020;
|
Loading…
Reference in New Issue
Block a user