mirror of
https://github.com/StanGirard/quivr.git
synced 2024-11-23 12:26:03 +03:00
Search for brains through the dropdown (#507)
This commit is contained in:
parent
e931d29017
commit
02272ab0ca
@ -15,7 +15,7 @@ export const Header = ({
|
||||
y: hidden ? "-100%" : "0%",
|
||||
transition: { ease: "circOut" },
|
||||
}}
|
||||
className="sticky top-0 w-full border-b border-b-black/10 dark:border-b-white/25 bg-white dark:bg-black z-[1200]"
|
||||
className="sticky top-0 w-full border-b border-b-black/10 dark:border-b-white/25 bg-white dark:bg-black z-20"
|
||||
>
|
||||
<nav className="max-w-screen-xl mx-auto py-1 flex items-center justify-between gap-8">
|
||||
{children}
|
||||
|
@ -1,146 +0,0 @@
|
||||
/* eslint-disable */
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { FaBrain } from "react-icons/fa";
|
||||
import { IoMdAdd } from "react-icons/io";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import Field from "@/lib/components/ui/Field";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
import { useEventTracking } from "@/services/analytics/useEventTracking";
|
||||
import { UUID } from "crypto";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { MdCheck, MdDelete } from "react-icons/md";
|
||||
|
||||
export const BrainsDropDown = (): JSX.Element => {
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const [newBrainName, setNewBrainName] = useState("");
|
||||
const { allBrains, createBrain, setActiveBrain, currentBrain, deleteBrain } =
|
||||
useBrainContext();
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const { track } = useEventTracking();
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setShowDropdown((prevState) => !prevState);
|
||||
void track("SHOW_BRAINS_DROPDOWN");
|
||||
};
|
||||
|
||||
const handleCreateBrain = () => {
|
||||
if (newBrainName.trim() === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
void createBrain(newBrainName);
|
||||
setNewBrainName(""); // Reset the new brain name input
|
||||
void track("BRAIN_CREATED");
|
||||
};
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node | null)
|
||||
) {
|
||||
setShowDropdown(false);
|
||||
}
|
||||
};
|
||||
|
||||
const changeBrains = (value: string) => {
|
||||
void track("CHANGE_BRAIN");
|
||||
setNewBrainName(value);
|
||||
};
|
||||
|
||||
const deteleBrains = (id: UUID) => {
|
||||
void track("DELETE_BRAIN");
|
||||
deleteBrain(id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Add the brain icon and dropdown */}
|
||||
<div className="relative ml-auto px-4 py-2" ref={dropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center focus:outline-none"
|
||||
onClick={toggleDropdown}
|
||||
>
|
||||
<FaBrain className="w-6 h-6" />
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{showDropdown && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -32, height: "0" }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
height: "13rem",
|
||||
}}
|
||||
exit={{ opacity: 0, y: -32, height: "0" }}
|
||||
transition={{ duration: 0.2, ease: "easeInOut" }}
|
||||
className="absolute right-0 mt-2 w-96 flex flex-col bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg shadow-lg"
|
||||
>
|
||||
{/* Option to create a new brain */}
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleCreateBrain();
|
||||
}}
|
||||
className="flex items-center gap-2 p-2"
|
||||
>
|
||||
<Field
|
||||
name="brainname"
|
||||
placeholder="Add a new brain"
|
||||
autoFocus
|
||||
onChange={(e) => changeBrains(e.target.value)}
|
||||
/>
|
||||
<Button type="submit" className="px-2 py-2">
|
||||
<IoMdAdd className="w-5 h-5" />
|
||||
</Button>
|
||||
</form>
|
||||
<div className="overflow-auto scrollbar flex flex-col h-full">
|
||||
{/* List of brains */}
|
||||
{allBrains.map((brain) => (
|
||||
<div
|
||||
key={brain.id}
|
||||
className="relative flex group items-center"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`flex flex-1 items-center gap-2 w-full text-left px-4 py-2 text-sm leading-5 text-gray-900 dark:text-gray-300 group-hover:bg-gray-100 dark:group-hover:bg-gray-700 group-focus:bg-gray-100 dark:group-focus:bg-gray-700 group-focus:outline-none transition-colors`}
|
||||
onClick={() => setActiveBrain({ ...brain })}
|
||||
>
|
||||
<span>
|
||||
<MdCheck
|
||||
style={{
|
||||
opacity: currentBrain?.id === brain.id ? 1 : 0,
|
||||
}}
|
||||
className="text-xl transition-opacity"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
</span>
|
||||
<span className="flex-1">{brain.name}</span>
|
||||
</button>
|
||||
<Button
|
||||
className="group-hover:opacity-100 opacity-0 absolute right-0 hover:text-red-500 transition-[colors,opacity]"
|
||||
onClick={() => deteleBrains(brain.id)}
|
||||
variant={"tertiary"}
|
||||
>
|
||||
<MdDelete className="text-xl" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,60 @@
|
||||
import { FormEvent, useState } from "react";
|
||||
import { MdAdd } from "react-icons/md";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import Field from "@/lib/components/ui/Field";
|
||||
import Modal from "@/lib/components/ui/Modal";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
|
||||
const AddBrainModal = (): JSX.Element => {
|
||||
const [newBrainName, setNewBrainName] = useState("");
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
const { createBrain } = useBrainContext();
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (newBrainName.trim() === "" || isPending) {
|
||||
return;
|
||||
}
|
||||
setIsPending(true);
|
||||
await createBrain(newBrainName);
|
||||
setNewBrainName("");
|
||||
setIsPending(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
Trigger={
|
||||
<Button variant={"secondary"}>
|
||||
Add New Brain
|
||||
<MdAdd className="text-xl" />
|
||||
</Button>
|
||||
}
|
||||
title="Add Brain"
|
||||
desc="Add a new brain"
|
||||
>
|
||||
<form
|
||||
onSubmit={(e) => void handleSubmit(e)}
|
||||
className="my-10 flex items-center gap-2"
|
||||
>
|
||||
<Field
|
||||
name="brainname"
|
||||
label="Enter a brain name"
|
||||
autoFocus
|
||||
placeholder="E.g. History notes"
|
||||
autoComplete="off"
|
||||
value={newBrainName}
|
||||
onChange={(e) => setNewBrainName(e.currentTarget.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button isLoading={isPending} className="self-end" type="submit">
|
||||
Create
|
||||
<MdAdd className="text-xl" />
|
||||
</Button>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export { AddBrainModal };
|
@ -0,0 +1,88 @@
|
||||
import { useState } from "react";
|
||||
import { FaBrain } from "react-icons/fa";
|
||||
import { MdCheck, MdDelete } from "react-icons/md";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import Field from "@/lib/components/ui/Field";
|
||||
import Popover from "@/lib/components/ui/Popover";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
|
||||
import { AddBrainModal } from "./AddBrainModal";
|
||||
|
||||
export const BrainsDropDown = (): JSX.Element => {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const { allBrains, setActiveBrain, currentBrain, deleteBrain } =
|
||||
useBrainContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Add the brain icon and dropdown */}
|
||||
<div className="relative ml-auto px-4 py-2">
|
||||
<Popover
|
||||
Trigger={
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center focus:outline-none"
|
||||
>
|
||||
<FaBrain className="w-6 h-6" />
|
||||
</button>
|
||||
}
|
||||
ActionTrigger={<AddBrainModal />}
|
||||
CloseTrigger={false}
|
||||
>
|
||||
<div>
|
||||
<Field
|
||||
name="brainsearch"
|
||||
placeholder="Search for a brain"
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
<div className="overflow-auto scrollbar flex flex-col h-48 mt-5">
|
||||
{/* List of brains */}
|
||||
{allBrains.map((brain) => {
|
||||
if (brain.name.includes(searchQuery)) {
|
||||
return (
|
||||
<div
|
||||
key={brain.id}
|
||||
className="relative flex group items-center"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`flex flex-1 items-center gap-2 w-full text-left px-4 py-2 text-sm leading-5 text-gray-900 dark:text-gray-300 group-hover:bg-gray-100 dark:group-hover:bg-gray-700 group-focus:bg-gray-100 dark:group-focus:bg-gray-700 group-focus:outline-none transition-colors`}
|
||||
onClick={() => {
|
||||
setActiveBrain({ ...brain });
|
||||
setSearchQuery("");
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<MdCheck
|
||||
style={{
|
||||
opacity: currentBrain?.id === brain.id ? 1 : 0,
|
||||
}}
|
||||
className="text-xl transition-opacity"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
</span>
|
||||
<span className="flex-1">{brain.name}</span>
|
||||
</button>
|
||||
<Button
|
||||
className="group-hover:visible invisible absolute right-0 hover:text-red-500 transition-[colors,opacity]"
|
||||
onClick={() => void deleteBrain(brain.id)}
|
||||
variant={"tertiary"}
|
||||
>
|
||||
<MdDelete className="text-xl" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
import { AddBrainModal } from "./AddBrainModal";
|
||||
import { BrainsDropDown } from "./BrainsDropDown";
|
||||
|
||||
export { BrainsDropDown, AddBrainModal };
|
71
frontend/lib/components/ui/Popover.tsx
Normal file
71
frontend/lib/components/ui/Popover.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { ReactNode, useState } from "react";
|
||||
|
||||
import Button from "./Button";
|
||||
|
||||
interface PopoverProps {
|
||||
children?: ReactNode;
|
||||
Trigger: ReactNode;
|
||||
ActionTrigger?: ReactNode;
|
||||
CloseTrigger?: ReactNode;
|
||||
}
|
||||
|
||||
const Popover = ({
|
||||
children,
|
||||
Trigger,
|
||||
ActionTrigger,
|
||||
CloseTrigger,
|
||||
}: PopoverProps): JSX.Element => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
|
||||
<PopoverPrimitive.Trigger asChild>{Trigger}</PopoverPrimitive.Trigger>
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<PopoverPrimitive.Portal forceMount>
|
||||
<PopoverPrimitive.Content forceMount asChild sideOffset={5}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -32 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
}}
|
||||
exit={{ opacity: 0, y: -32 }}
|
||||
transition={{ duration: 0.2, ease: "easeInOut" }}
|
||||
className="relative flex flex-col p-4 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg shadow-lg z-40"
|
||||
>
|
||||
<div className="flex-1">{children}</div>
|
||||
<div className="mt-4 self-end flex gap-4">
|
||||
{ActionTrigger !== undefined && (
|
||||
<PopoverPrimitive.Close asChild>
|
||||
{ActionTrigger}
|
||||
</PopoverPrimitive.Close>
|
||||
)}
|
||||
<PopoverPrimitive.Close asChild>
|
||||
{CloseTrigger === undefined ? (
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
className="px-3 py-2"
|
||||
aria-label="Close"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
) : (
|
||||
CloseTrigger
|
||||
)}
|
||||
</PopoverPrimitive.Close>
|
||||
</div>
|
||||
<PopoverPrimitive.Arrow className="fill-white stroke-gray-300 stroke-2" />
|
||||
</motion.div>
|
||||
</PopoverPrimitive.Content>
|
||||
</PopoverPrimitive.Portal>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</PopoverPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default Popover;
|
@ -10,6 +10,7 @@ import {
|
||||
getUserDefaultBrainFromBackend,
|
||||
} from "@/lib/api";
|
||||
import { useAxios, useToast } from "@/lib/hooks";
|
||||
import { useEventTracking } from "@/services/analytics/useEventTracking";
|
||||
|
||||
import {
|
||||
getBrainFromLocalStorage,
|
||||
@ -36,6 +37,8 @@ export const useBrainState = (): BrainStateProps => {
|
||||
const [currentBrainId, setCurrentBrainId] = useState<null | UUID>(null);
|
||||
const { axiosInstance } = useAxios();
|
||||
|
||||
const { track } = useEventTracking();
|
||||
|
||||
const currentBrain = allBrains.find((brain) => brain.id === currentBrainId);
|
||||
|
||||
// options: Record<string, string | unknown>;
|
||||
@ -45,6 +48,7 @@ export const useBrainState = (): BrainStateProps => {
|
||||
if (createdBrain !== undefined) {
|
||||
setAllBrains((prevBrains) => [...prevBrains, createdBrain]);
|
||||
saveBrainInLocalStorage(createdBrain);
|
||||
void track("BRAIN_CREATED");
|
||||
|
||||
return createdBrain.id;
|
||||
} else {
|
||||
@ -58,6 +62,7 @@ export const useBrainState = (): BrainStateProps => {
|
||||
const deleteBrain = async (id: UUID) => {
|
||||
await deleteBrainFromBE(axiosInstance, id);
|
||||
setAllBrains((prevBrains) => prevBrains.filter((brain) => brain.id !== id));
|
||||
void track("DELETE_BRAIN");
|
||||
};
|
||||
|
||||
const getBrainWithId = async (brainId: UUID): Promise<Brain> => {
|
||||
@ -90,6 +95,7 @@ export const useBrainState = (): BrainStateProps => {
|
||||
saveBrainInLocalStorage(newActiveBrain);
|
||||
setCurrentBrainId(id);
|
||||
console.log("Setting active brain", newActiveBrain);
|
||||
void track("CHANGE_BRAIN");
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
@ -21,6 +21,7 @@
|
||||
"@growthbook/growthbook-react": "^0.17.0",
|
||||
"@june-so/analytics-next": "^2.0.0",
|
||||
"@radix-ui/react-dialog": "^1.0.3",
|
||||
"@radix-ui/react-popover": "^1.0.6",
|
||||
"@radix-ui/react-toast": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.6",
|
||||
"@sentry/nextjs": "^7.57.0",
|
||||
|
@ -840,6 +840,28 @@
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-use-layout-effect" "1.0.1"
|
||||
|
||||
"@radix-ui/react-popover@^1.0.6":
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.6.tgz#19bb81e7450482c625b8cd05bf4dcd1d2cd91a8b"
|
||||
integrity sha512-cZ4defGpkZ0qTRtlIBzJLSzL6ht7ofhhW4i1+pkemjV1IKXm0wgCRnee154qlV6r9Ttunmh2TNZhMfV2bavUyA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.1"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-context" "1.0.1"
|
||||
"@radix-ui/react-dismissable-layer" "1.0.4"
|
||||
"@radix-ui/react-focus-guards" "1.0.1"
|
||||
"@radix-ui/react-focus-scope" "1.0.3"
|
||||
"@radix-ui/react-id" "1.0.1"
|
||||
"@radix-ui/react-popper" "1.1.2"
|
||||
"@radix-ui/react-portal" "1.0.3"
|
||||
"@radix-ui/react-presence" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-slot" "1.0.2"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||
aria-hidden "^1.1.1"
|
||||
react-remove-scroll "2.5.5"
|
||||
|
||||
"@radix-ui/react-popper@1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.2.tgz#4c0b96fcd188dc1f334e02dba2d538973ad842e9"
|
||||
|
Loading…
Reference in New Issue
Block a user