diff --git a/backend/modules/user/dto/inputs.py b/backend/modules/user/dto/inputs.py index 3751ebb9c..78d837d08 100644 --- a/backend/modules/user/dto/inputs.py +++ b/backend/modules/user/dto/inputs.py @@ -8,3 +8,6 @@ class UserUpdatableProperties(BaseModel): username: Optional[str] = None company: Optional[str] = None onboarded: Optional[bool] = None + company_size: Optional[str] = None + usage_purpose: Optional[str] = None + diff --git a/backend/modules/user/entity/user_identity.py b/backend/modules/user/entity/user_identity.py index 6ea2c0c52..06c8ab466 100644 --- a/backend/modules/user/entity/user_identity.py +++ b/backend/modules/user/entity/user_identity.py @@ -10,3 +10,5 @@ class UserIdentity(BaseModel): username: Optional[str] = None company: Optional[str] = None onboarded: Optional[bool] = None + company_size: Optional[str] = None + usage_purpose: Optional[str] = None diff --git a/frontend/app/App.tsx b/frontend/app/App.tsx index c56b85efd..5c409a879 100644 --- a/frontend/app/App.tsx +++ b/frontend/app/App.tsx @@ -18,6 +18,7 @@ import { import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext"; import { ChatsProvider } from "@/lib/context/ChatsProvider"; import { MenuProvider } from "@/lib/context/MenuProvider/Menu-provider"; +import { OnboardingProvider } from "@/lib/context/OnboardingProvider/Onboarding-provider"; import { SearchModalProvider } from "@/lib/context/SearchModalProvider/search-modal-provider"; import { useSupabase } from "@/lib/context/SupabaseProvider"; import { UserSettingsProvider } from "@/lib/context/UserSettingsProvider/User-settings.provider"; @@ -96,11 +97,13 @@ const AppWithQueryClient = ({ children }: PropsWithChildren): JSX.Element => { - - - {children} - - + + + + {children} + + + diff --git a/frontend/app/search/page.tsx b/frontend/app/search/page.tsx index 5a623d464..98f387d6d 100644 --- a/frontend/app/search/page.tsx +++ b/frontend/app/search/page.tsx @@ -5,6 +5,7 @@ import { useEffect } from "react"; import { QuivrLogo } from "@/lib/assets/QuivrLogo"; import { AddBrainModal } from "@/lib/components/AddBrainModal"; import { useBrainCreationContext } from "@/lib/components/AddBrainModal/brainCreation-provider"; +import { OnboardingModal } from "@/lib/components/OnboardingModal/OnboardingModal"; import PageHeader from "@/lib/components/PageHeader/PageHeader"; import { UploadDocumentModal } from "@/lib/components/UploadDocumentModal/UploadDocumentModal"; import { SearchBar } from "@/lib/components/ui/SearchBar/SearchBar"; @@ -66,6 +67,7 @@ const Search = (): JSX.Element => { + ); }; diff --git a/frontend/app/studio/[brainId]/BrainManagementTabs/components/SettingsTab/components/ModelSelection/ModelSelection.tsx b/frontend/app/studio/[brainId]/BrainManagementTabs/components/SettingsTab/components/ModelSelection/ModelSelection.tsx index 498f81baf..27f2ef6c7 100644 --- a/frontend/app/studio/[brainId]/BrainManagementTabs/components/SettingsTab/components/ModelSelection/ModelSelection.tsx +++ b/frontend/app/studio/[brainId]/BrainManagementTabs/components/SettingsTab/components/ModelSelection/ModelSelection.tsx @@ -43,7 +43,7 @@ export const ModelSelection = (props: ModelSelectionProps): JSX.Element => { void handleSubmit(false); }} selectedOption={{ value: model, label: model }} - placeholder="hey" + placeholder="Select a model" iconName="robot" /> diff --git a/frontend/app/user/components/ApiKeyConfig/hooks/useApiKeyConfig.ts b/frontend/app/user/components/ApiKeyConfig/hooks/useApiKeyConfig.ts index 22e2c8914..3989f4cf9 100644 --- a/frontend/app/user/components/ApiKeyConfig/hooks/useApiKeyConfig.ts +++ b/frontend/app/user/components/ApiKeyConfig/hooks/useApiKeyConfig.ts @@ -63,7 +63,10 @@ export const useApiKeyConfig = () => { try { setChangeOpenAiApiKeyRequestPending(true); - await updateUserIdentity({}); + await updateUserIdentity({ + username: userIdentity?.username ?? "", + onboarded: userIdentity?.onboarded ?? false, + }); void queryClient.invalidateQueries({ queryKey: [USER_IDENTITY_DATA_KEY], }); @@ -82,7 +85,10 @@ export const useApiKeyConfig = () => { const removeOpenAiApiKey = async () => { try { setChangeOpenAiApiKeyRequestPending(true); - await updateUserIdentity({}); + await updateUserIdentity({ + username: userIdentity?.username ?? "", + onboarded: userIdentity?.onboarded ?? false, + }); publish({ variant: "success", diff --git a/frontend/lib/api/user/user.ts b/frontend/lib/api/user/user.ts index 5319da3f1..6b62fa5c5 100644 --- a/frontend/lib/api/user/user.ts +++ b/frontend/lib/api/user/user.ts @@ -3,12 +3,37 @@ import { UUID } from "crypto"; import { UserStats } from "@/lib/types/User"; +export enum CompanySize { + One = "1-10", + Two = "10-25", + Three = "25-50", + Four = "50-100", + Five = "100-500", + Six = "500-1000", + Seven = "1000-5000", + Eight = "+5000", +} + +export enum UsagePurpose { + Business = "Business", + NGO = "NGO", + Personal = "Personal", + Student = "Student", + Teacher = "Teacher", +} + export type UserIdentityUpdatableProperties = { - empty?: string | null; + username: string; + company?: string; + onboarded: boolean; + company_size?: CompanySize; + usage_purpose?: UsagePurpose; }; export type UserIdentity = { user_id: UUID; + onboarded: boolean; + username: string; }; export const updateUserIdentity = async ( diff --git a/frontend/lib/components/AddBrainModal/components/BrainMainInfosStep/BrainMainInfosStep.tsx b/frontend/lib/components/AddBrainModal/components/BrainMainInfosStep/BrainMainInfosStep.tsx index 2d1bbe14f..093b3443a 100644 --- a/frontend/lib/components/AddBrainModal/components/BrainMainInfosStep/BrainMainInfosStep.tsx +++ b/frontend/lib/components/AddBrainModal/components/BrainMainInfosStep/BrainMainInfosStep.tsx @@ -1,6 +1,7 @@ import { Controller, useFormContext } from "react-hook-form"; import { CreateBrainProps } from "@/lib/components/AddBrainModal/types/types"; +import { FieldHeader } from "@/lib/components/ui/FieldHeader/FieldHeader"; import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton"; import { TextAreaInput } from "@/lib/components/ui/TextAreaInput/TextAreaInput"; import { TextInput } from "@/lib/components/ui/TextInput/TextInput"; @@ -35,26 +36,36 @@ export const BrainMainInfosStep = (): JSX.Element => {
Define brain identity - ( - - )} - /> - ( - - )} - /> +
+ + ( + + )} + /> +
+
+ + ( + + )} + /> +
{ const pathname = usePathname() ?? ""; const isSelected = pathname.includes("/user"); + const { userIdentityData } = useUserData(); + + let username = userIdentityData?.username ?? "Profile"; + + useEffect(() => { + username = userIdentityData?.username ?? "Profile"; + }, [userIdentityData]); return ( { + const { isOnboardingModalOpened, setIsOnboardingModalOpened } = + useOnboardingContext(); + + const methods = useForm({ + defaultValues: { + username: "", + companyName: "", + companySize: undefined, + usagePurpose: "", + }, + }); + const { watch } = methods; + const username = watch("username"); + + const { updateUserIdentity } = useUserApi(); + + const companySizeOptions = Object.entries(CompanySize).map(([, value]) => ({ + label: value, + value: value, + })); + + const usagePurposeOptions = Object.entries(UsagePurpose).map( + ([key, value]) => ({ + label: value, + value: key, + }) + ); + + const submitForm = async () => { + await updateUserIdentity({ + username: methods.getValues("username"), + company: methods.getValues("companyName"), + onboarded: true, + company_size: methods.getValues("companySize"), + usage_purpose: methods.getValues("usagePurpose") as + | UsagePurpose + | undefined, + }); + window.location.reload(); + }; + + return ( + + } + unclosable={true} + > +
+
+
+ + ( + + )} + /> +
+
+ + ( + + )} + /> +
+
+ + ( + + )} + /> +
+
+ + ( + + )} + /> +
+
+
+ submitForm()} + disabled={!username} + /> +
+
+
+
+ ); +}; diff --git a/frontend/lib/components/OnboardingModal/types/types.ts b/frontend/lib/components/OnboardingModal/types/types.ts new file mode 100644 index 000000000..995825ba3 --- /dev/null +++ b/frontend/lib/components/OnboardingModal/types/types.ts @@ -0,0 +1,8 @@ +import { CompanySize } from "@/lib/api/user/user"; + +export type OnboardingProps = { + username: string; + companyName: string; + companySize: CompanySize; + usagePurpose: string; +}; diff --git a/frontend/lib/components/ui/FieldHeader/FieldHeader.module.scss b/frontend/lib/components/ui/FieldHeader/FieldHeader.module.scss index 6d1b5e44c..9f75b01ca 100644 --- a/frontend/lib/components/ui/FieldHeader/FieldHeader.module.scss +++ b/frontend/lib/components/ui/FieldHeader/FieldHeader.module.scss @@ -1,3 +1,4 @@ +@use "@/styles/Colors.module.scss"; @use "@/styles/Spacings.module.scss"; .field_header_wrapper { @@ -6,4 +7,8 @@ font-weight: 500; align-items: center; padding-bottom: Spacings.$spacing02; + + .mandatory { + color: Colors.$dangerous; + } } diff --git a/frontend/lib/components/ui/FieldHeader/FieldHeader.tsx b/frontend/lib/components/ui/FieldHeader/FieldHeader.tsx index df103ae67..0d29c369b 100644 --- a/frontend/lib/components/ui/FieldHeader/FieldHeader.tsx +++ b/frontend/lib/components/ui/FieldHeader/FieldHeader.tsx @@ -8,17 +8,21 @@ type FieldHeaderProps = { iconName: string; label: string; help?: string; + mandatory?: boolean; }; export const FieldHeader = ({ iconName, label, help, + mandatory, }: FieldHeaderProps): JSX.Element => { return (
+ {mandatory && *} + {help && "*"} {help && (
diff --git a/frontend/lib/components/ui/Modal/Modal.module.scss b/frontend/lib/components/ui/Modal/Modal.module.scss index 800aaba4b..15a1c09f3 100644 --- a/frontend/lib/components/ui/Modal/Modal.module.scss +++ b/frontend/lib/components/ui/Modal/Modal.module.scss @@ -24,6 +24,8 @@ box-shadow: BoxShadow.$medium; max-width: 90vw; overflow: scroll; + width: 35vw; + height: 80vh; &.big_modal { width: 40vw; @@ -36,9 +38,7 @@ } @media (max-width: ScreenSizes.$small) { - &.big_modal { - width: 90vw; - } + width: 90vw; } .close_button_wrapper { diff --git a/frontend/lib/components/ui/Modal/Modal.tsx b/frontend/lib/components/ui/Modal/Modal.tsx index ef21ff794..a2b59beaa 100644 --- a/frontend/lib/components/ui/Modal/Modal.tsx +++ b/frontend/lib/components/ui/Modal/Modal.tsx @@ -20,6 +20,7 @@ type CommonModalProps = { isOpen?: undefined; setOpen?: undefined; bigModal?: boolean; + unclosable?: boolean; unforceWhite?: boolean; }; @@ -30,6 +31,31 @@ type ModalProps = setOpen: (isOpen: boolean) => void; }); +const handleInteractOutside = (unclosable: boolean, event: Event) => { + if (unclosable) { + event.preventDefault(); + } +}; + +const handleModalContentAnimation = ( + isOpen: boolean, + bigModal: boolean, + unforceWhite: boolean +) => { + const initialAnimation = { opacity: 0, y: "-40%" }; + const animateAnimation = { opacity: 1, y: "0%" }; + const exitAnimation = { opacity: 0, y: "40%" }; + + return { + initial: initialAnimation, + animate: animateAnimation, + exit: exitAnimation, + className: `${styles.modal_content_wrapper} ${ + bigModal ? styles.big_modal : "" + } ${unforceWhite ? styles.white : ""}`, + }; +}; + export const Modal = ({ title, desc, @@ -39,6 +65,7 @@ export const Modal = ({ isOpen: customIsOpen, setOpen: customSetOpen, bigModal, + unclosable, unforceWhite, }: ModalProps): JSX.Element => { const [isOpen, setOpen] = useState(false); @@ -62,14 +89,19 @@ export const Modal = ({ animate={{ opacity: 1 }} exit={{ opacity: 0 }} > - + + handleInteractOutside(!!unclosable, event) + } + > )} - - - + {!unclosable && ( + + + + )} diff --git a/frontend/lib/context/OnboardingProvider/Onboarding-provider.tsx b/frontend/lib/context/OnboardingProvider/Onboarding-provider.tsx new file mode 100644 index 000000000..a0e93ca6e --- /dev/null +++ b/frontend/lib/context/OnboardingProvider/Onboarding-provider.tsx @@ -0,0 +1,36 @@ +import { createContext, useEffect, useState } from "react"; + +import { useUserData } from "@/lib/hooks/useUserData"; + +export type OnboardingContextType = { + isOnboardingModalOpened: boolean; + setIsOnboardingModalOpened: React.Dispatch>; +}; + +export const OnboardingContext = createContext< + OnboardingContextType | undefined +>(undefined); + +export const OnboardingProvider = ({ + children, +}: { + children: React.ReactNode; +}): JSX.Element => { + const [isOnboardingModalOpened, setIsOnboardingModalOpened] = useState(false); + const { userIdentityData } = useUserData(); + + useEffect(() => { + setIsOnboardingModalOpened(!!userIdentityData?.onboarded); + }, []); + + return ( + + {children} + + ); +}; diff --git a/frontend/lib/context/OnboardingProvider/hooks/useOnboardingContext.tsx b/frontend/lib/context/OnboardingProvider/hooks/useOnboardingContext.tsx new file mode 100644 index 000000000..b4adeb811 --- /dev/null +++ b/frontend/lib/context/OnboardingProvider/hooks/useOnboardingContext.tsx @@ -0,0 +1,17 @@ +import { useContext } from "react"; + +import { + OnboardingContext, + OnboardingContextType, +} from "../Onboarding-provider"; + +export const useOnboardingContext = (): OnboardingContextType => { + const context = useContext(OnboardingContext); + if (context === undefined) { + throw new Error( + "useOnboardingContext must be used within a OnboardingProvider" + ); + } + + return context; +}; diff --git a/frontend/lib/helpers/iconList.ts b/frontend/lib/helpers/iconList.ts index 6644f1283..c39af2aae 100644 --- a/frontend/lib/helpers/iconList.ts +++ b/frontend/lib/helpers/iconList.ts @@ -27,9 +27,11 @@ import { } from "react-icons/fa"; import { FaInfo } from "react-icons/fa6"; import { FiUpload } from "react-icons/fi"; +import { HiBuildingOffice } from "react-icons/hi2"; import { IoIosAdd, IoIosHelpCircleOutline, + IoIosRadio, IoMdClose, IoMdLogOut, } from "react-icons/io"; @@ -50,6 +52,7 @@ import { LuChevronLeft, LuChevronRight, LuCopy, + LuGoal, LuPlusCircle, LuSearch, } from "react-icons/lu"; @@ -63,6 +66,7 @@ import { MdOutlineModeEditOutline, MdUploadFile, } from "react-icons/md"; +import { PiOfficeChairFill } from "react-icons/pi"; import { RiHashtag } from "react-icons/ri"; import { SlOptions } from "react-icons/sl"; import { TbNetwork } from "react-icons/tb"; @@ -73,6 +77,7 @@ export const iconList: { [name: string]: IconType } = { addWithoutCircle: IoIosAdd, brain: LuBrain, brainCircuit: LuBrainCircuit, + chair: PiOfficeChairFill, chat: BsChatLeftText, check: FaCheck, checkCircle: FaCheckCircle, @@ -93,6 +98,7 @@ export const iconList: { [name: string]: IconType } = { flag: CiFlag1, followUp: IoArrowUpCircleOutline, github: FaGithub, + goal: LuGoal, graph: VscGraph, hashtag: RiHashtag, help: IoIosHelpCircleOutline, @@ -105,10 +111,12 @@ export const iconList: { [name: string]: IconType } = { loader: AiOutlineLoading3Quarters, logout: IoMdLogOut, moon: FaMoon, + office: HiBuildingOffice, options: SlOptions, paragraph: BsTextParagraph, prompt: FaRegKeyboard, redirection: BsArrowRightShort, + radio: IoIosRadio, robot: LiaRobotSolid, search: LuSearch, settings: IoSettingsSharp, diff --git a/frontend/lib/hooks/useUserData.ts b/frontend/lib/hooks/useUserData.ts index 0be4324dd..4c204b4de 100644 --- a/frontend/lib/hooks/useUserData.ts +++ b/frontend/lib/hooks/useUserData.ts @@ -1,18 +1,25 @@ import { useQuery } from "@tanstack/react-query"; -import { USER_DATA_KEY } from "../api/user/config"; +import { USER_DATA_KEY, USER_IDENTITY_DATA_KEY } from "../api/user/config"; import { useUserApi } from "../api/user/useUserApi"; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export const useUserData = () => { const { getUser } = useUserApi(); + const { getUserIdentity } = useUserApi(); const { data: userData } = useQuery({ queryKey: [USER_DATA_KEY], queryFn: getUser, }); + const { data: userIdentityData } = useQuery({ + queryKey: [USER_IDENTITY_DATA_KEY], + queryFn: getUserIdentity, + }); + return { userData, + userIdentityData, }; }; diff --git a/supabase/migrations/20240314005817_user_identity_company_info.sql b/supabase/migrations/20240314005817_user_identity_company_info.sql new file mode 100644 index 000000000..93323709b --- /dev/null +++ b/supabase/migrations/20240314005817_user_identity_company_info.sql @@ -0,0 +1,7 @@ +create type "public"."user_identity_company_size" as enum ('1-10', '10-25', '25-50', '50-100', '100-250', '250-500', '500-1000', '1000-5000', '+5000'); + +alter table "public"."user_identity" add column "company_size" user_identity_company_size; + +alter table "public"."user_identity" add column "role_in_company" text; + + diff --git a/supabase/migrations/20240316195514_usage_purpose.sql b/supabase/migrations/20240316195514_usage_purpose.sql new file mode 100644 index 000000000..657e89e43 --- /dev/null +++ b/supabase/migrations/20240316195514_usage_purpose.sql @@ -0,0 +1,5 @@ +alter table "public"."user_identity" drop column "role_in_company"; + +alter table "public"."user_identity" add column "usage_purpose" text; + +