feat: add new actions modal (#1870)

Issue: https://github.com/StanGirard/quivr/issues/1861
- Update Quivr font
- Add Actions modal
- Update Popover component
- Move Chat config to Actions modal

Demo:


https://github.com/StanGirard/quivr/assets/63923024/df3ac138-6950-46fe-8e40-6276005c7ef1
This commit is contained in:
Mamadou DICKO 2023-12-13 08:54:35 +01:00 committed by GitHub
parent f28e009d98
commit a30042f0fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 170 additions and 177 deletions

View File

@ -0,0 +1,42 @@
import { PopoverAnchor } from "@radix-ui/react-popover";
import { useState } from "react";
import { LuPlusCircle, LuXCircle } from "react-icons/lu";
import Button from "@/lib/components/ui/Button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/lib/components/ui/Popover";
import { ConfigModal } from "./components/ConfigModal";
export const ActionsModal = (): JSX.Element => {
const [isActionsModalOpened, setIsActionsModalOpened] = useState(false);
const Icon = isActionsModalOpened ? LuXCircle : LuPlusCircle;
return (
<div className="flex items-center">
<Popover
open={isActionsModalOpened}
onOpenChange={(isOpened) => setIsActionsModalOpened(isOpened)}
>
<PopoverTrigger>
<PopoverAnchor asChild>
<Button variant="tertiary" type="button" className="p-0">
<Icon className="text-accent font-bold" size={30} />
</Button>
</PopoverAnchor>
</PopoverTrigger>
<PopoverContent
align="end"
sideOffset={15}
className="min-h-[200px] w-[200px]"
>
<ConfigModal />
</PopoverContent>
</Popover>
</div>
);
};

View File

@ -0,0 +1,42 @@
import { forwardRef } from "react";
import CoreButton, {
ButtonProps as CoreButtonProps,
} from "@/lib/components/ui/Button";
import { cn } from "@/lib/utils";
type ButtonProps = CoreButtonProps & {
onClick?: () => void;
className?: string;
label?: string;
startIcon?: JSX.Element;
endIcon?: JSX.Element;
};
export const Button = forwardRef(
(
{ onClick, className, label, startIcon, endIcon, ...props }: ButtonProps,
forwardedRef
): JSX.Element => {
return (
<CoreButton
className={cn("p-2 sm:px-3 text-primary focus:ring-0 ", className)}
variant={"tertiary"}
data-testid="config-button"
ref={forwardedRef}
onClick={onClick}
{...props}
>
<div className="flex flex-row justify-between w-full items-center">
<div className="flex flex-row gap-2 items-center">
{startIcon}
<span className="hidden sm:block">{label}</span>
</div>
{endIcon}
</div>
</CoreButton>
);
}
);
Button.displayName = CoreButton.displayName;

View File

@ -1,12 +1,12 @@
/* eslint-disable max-lines */
import { useTranslation } from "react-i18next";
import { MdCheck, MdSettings } from "react-icons/md";
import { LuChevronRight, LuSettings } from "react-icons/lu";
import { MdCheck } from "react-icons/md";
import Button from "@/lib/components/ui/Button";
import { Modal } from "@/lib/components/ui/Modal";
import { defineMaxTokens } from "@/lib/helpers/defineMaxTokens";
import { useConfigModal } from "./hooks/useConfigModal";
import { Button } from "../Button";
export const ConfigModal = (): JSX.Element => {
const {
@ -24,12 +24,11 @@ export const ConfigModal = (): JSX.Element => {
<Modal
Trigger={
<Button
className="p-2 sm:px-3"
variant={"tertiary"}
data-testid="config-button"
>
<MdSettings className="text-lg sm:text-xl lg:text-2xl" />
</Button>
label={"Parametres"}
startIcon={<LuSettings size={18} />}
endIcon={<LuChevronRight size={18} />}
className="w-full"
/>
}
title="Chat configuration"
desc="Adjust your chat settings"
@ -68,16 +67,16 @@ export const ConfigModal = (): JSX.Element => {
</fieldset>
<Button
className="mt-12 self-end"
className="mt-12 self-end text-white"
type="button"
onClick={() => {
handleSubmit();
setIsConfigModalOpen(false);
}}
>
Save
<MdCheck className="text-xl" />
</Button>
variant={"primary"}
label="Save"
endIcon={<MdCheck className="text-xl" />}
/>
</form>
</Modal>
);

View File

@ -1,2 +1 @@
export * from "./ConfigModal";
export * from "./OnboardingQuestions";

View File

@ -7,8 +7,8 @@ import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider
import { getBrainIconFromBrainType } from "@/lib/helpers/getBrainIconFromBrainType";
import { OnboardingQuestions } from "./components";
import { ActionsModal } from "./components/ActionsModal/ActionsModal";
import { ChatEditor } from "./components/ChatEditor/ChatEditor";
import { ConfigModal } from "./components/ConfigModal";
import { useChatInput } from "./hooks/useChatInput";
type ChatInputProps = {
@ -58,9 +58,9 @@ export const ChatInput = ({
/>
</div>
<div className="flex flex-row items-end">
<div className="flex flex-row items-center gap-4">
<Button
className="px-3 py-2 sm:px-4 sm:py-2"
className="px-3 py-2 sm:px-4 sm:py-2 bg-primary border-0"
type="submit"
isLoading={generatingAnswer}
data-testid="submit-button"
@ -69,9 +69,7 @@ export const ChatInput = ({
? t("thinking", { ns: "chat" })
: t("chat", { ns: "chat" })}
</Button>
<div className="hidden md:flex items-center">
<ConfigModal />
</div>
<ActionsModal />
</div>
</form>
</div>

View File

@ -1,6 +1,6 @@
import { createServerComponentSupabaseClient } from "@supabase/auth-helpers-nextjs";
import { Analytics as VercelAnalytics } from "@vercel/analytics/react";
import { Inter } from "next/font/google";
import { Outfit } from "next/font/google";
import { cookies, headers } from "next/headers";
import { ToastProvider } from "@/lib/components/ui/Toast";
@ -10,7 +10,7 @@ import { SupabaseProvider } from "@/lib/context/SupabaseProvider";
import { App } from "./App";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
const inter = Outfit({ subsets: ["latin"], weight: "400" });
export const metadata = {
title: "Quivr - Get a Second Brain with Generative AI",

View File

@ -1,59 +0,0 @@
import { FaLanguage } from "react-icons/fa";
import { MdCheck } from "react-icons/md";
import Popover from "@/lib/components/ui/Popover";
import { useLanguageHook } from "./hooks/useLanguageHook";
export const LanguageDropDown = (): JSX.Element => {
const { allLanguages, currentLanguage, change } = useLanguageHook();
return (
<>
{/* Add the brain icon and dropdown */}
<div className="focus:outline-none text-3xl">
<Popover
Trigger={
<button
type="button"
className="flex items-center focus:outline-none"
>
<FaLanguage className="w-6 h-6" />
</button>
}
CloseTrigger={false}
>
<div>
<div className="overflow-auto scrollbar flex flex-col h-48 mt-5">
{Object.keys(allLanguages).map((lang) => {
return (
<div key={lang} 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={() => {
change(lang);
}}
>
<span>
<MdCheck
style={{
opacity: currentLanguage === lang ? 1 : 0,
}}
className="text-xl transition-opacity"
width={32}
height={32}
/>
</span>
<span className="flex-1">{allLanguages[lang].label}</span>
</button>
</div>
);
})}
</div>
</div>
</Popover>
</div>
</>
);
};

View File

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

View File

@ -9,7 +9,7 @@ import { redirectToLogin } from "@/lib/router/redirectToLogin";
import { StripePricingOrManageButton, UserStatistics } from "./components";
import { ApiKeyConfig } from "./components/ApiKeyConfig";
import LanguageSelect from "./components/LanguageDropDown/LanguageSelect";
import LanguageSelect from "./components/LanguageSelect/LanguageSelect";
import { LogoutModal } from "./components/LogoutCard/LogoutModal";
import ThemeSelect from "./components/ThemeSelect/ThemeSelect";

View File

@ -70,6 +70,8 @@ export const UserToInvite = ({
onChange={setSelectedRole}
value={selectedRole}
options={translatedOptions}
popoverSide="bottom"
popoverClassName="w-36"
/>
</div>
);

View File

@ -68,7 +68,6 @@ const Button = forwardRef(
{children} {isLoading && <FaSpinner className="animate-spin" />}
</>
);
const buttonElement = <button {...buttonProps}>buttonChildren</button>;
if (tooltip !== undefined) {
return (

View File

@ -1,71 +1,29 @@
"use client";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { AnimatePresence, motion } from "framer-motion";
import { ReactNode, useState } from "react";
import * as React from "react";
import Button from "./Button";
import { cn } from "@/lib/utils";
interface PopoverProps {
children?: ReactNode;
Trigger: ReactNode;
ActionTrigger?: ReactNode;
CloseTrigger?: ReactNode;
}
const Popover = PopoverPrimitive.Root;
const Popover = ({
children,
Trigger,
ActionTrigger,
CloseTrigger,
}: PopoverProps): JSX.Element => {
const [open, setOpen] = useState(false);
const PopoverTrigger = PopoverPrimitive.Trigger;
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-50 md: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>
);
};
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-white p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export default Popover;
export { Popover, PopoverContent, PopoverTrigger };

View File

@ -1,7 +1,13 @@
/*eslint complexity: ["error", 10]*/
/* eslint-disable max-lines */
import { BsCheckCircleFill } from "react-icons/bs";
import Popover from "@/lib/components/ui/Popover";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/lib/components/ui/Popover";
import { cn } from "@/lib/utils";
export type SelectOptionProps<T> = {
@ -17,6 +23,8 @@ type SelectProps<T> = {
readOnly?: boolean;
className?: string;
emptyLabel?: string;
popoverClassName?: string;
popoverSide?: "top" | "bottom" | "left" | "right" | undefined;
};
const selectedStyle = "rounded-lg bg-black text-white";
@ -29,6 +37,8 @@ export const Select = <T extends string | number>({
readOnly = false,
className,
emptyLabel,
popoverClassName,
popoverSide,
}: SelectProps<T>): JSX.Element => {
const selectedValueLabel = options.find(
(option) => option.value === value
@ -74,8 +84,8 @@ export const Select = <T extends string | number>({
</label>
)}
<div className="relative">
<Popover
Trigger={
<Popover>
<PopoverTrigger>
<button
type="button"
className="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 sm:text-sm sm:leading-6"
@ -101,30 +111,33 @@ export const Select = <T extends string | number>({
</svg>
</span>
</button>
}
CloseTrigger={<div />}
>
<ul role="listbox">
{options.map((option) => (
<li
className="text-gray-900 relative cursor-pointer select-none py-2"
id="listbox-option-0"
key={option.value}
onClick={() => onChange(option.value)}
>
<div
className={`flex items-center px-3 py-2 ${
value === option.value && selectedStyle
}`}
</PopoverTrigger>
<PopoverContent
className={cn("max-h-[200px] overflow-scroll", popoverClassName)}
side={popoverSide ?? "top"}
>
<ul role="listbox">
{options.map((option) => (
<li
className="text-gray-900 relative cursor-pointer select-none py-0"
id="listbox-option-0"
key={option.value}
onClick={() => onChange(option.value)}
>
<span className="font-bold block truncate mr-2">
{option.label}
</span>
{value === option.value && <BsCheckCircleFill />}
</div>
</li>
))}
</ul>
<div
className={`flex items-center px-3 py-2 ${
value === option.value && selectedStyle
}`}
>
<span className="font-bold block truncate mr-2">
{option.label}
</span>
{value === option.value && <BsCheckCircleFill />}
</div>
</li>
))}
</ul>
</PopoverContent>
</Popover>
</div>
</div>

View File

@ -16,7 +16,8 @@ module.exports = {
},
colors: {
black: "#11243E",
primary: "#7A27FD",
primary: "#6142D4",
accent: "#13ABBA",
"chat-bg-gray": "#D9D9D9",
"msg-gray": "#9B9B9B",
"msg-header-gray": "#8F8F8F",