feat(frontend): onboarding V2 (#2394)

# 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):

---------

Co-authored-by: Stan Girard <girard.stanislas@gmail.com>
This commit is contained in:
Antoine Dewez 2024-04-09 18:06:33 +02:00 committed by GitHub
parent 6acf5fca5f
commit 7ff9abee1a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 203 additions and 222 deletions

View File

@ -58,16 +58,6 @@ async def retrieve_public_brains() -> list[PublicBrain]:
return brain_service.get_public_brains()
@brain_router.get(
"/brains/default/", dependencies=[Depends(AuthBearer())], tags=["Brain"]
)
async def retrieve_default_brain(
current_user: UserIdentity = Depends(get_current_user),
):
"""Retrieve or create the default brain for the current user."""
brain = brain_user_service.get_default_user_brain_or_create_new(current_user)
return {"id": brain.brain_id, "name": brain.name, "rights": "Owner"}
@brain_router.get(
"/brains/{brain_id}/",
@ -220,19 +210,6 @@ async def update_existing_brain_secrets(
return {"message": f"Brain {brain_id} has been updated."}
@brain_router.post(
"/brains/{brain_id}/default",
dependencies=[Depends(AuthBearer()), Depends(has_brain_authorization())],
tags=["Brain"],
)
async def set_brain_as_default(
brain_id: UUID, user: UserIdentity = Depends(get_current_user)
):
"""Set a brain as the default for the current user."""
brain_user_service.set_as_default_brain_for_user(user.id, brain_id)
return {"message": f"Brain {brain_id} has been set as default brain."}
@brain_router.post(
"/brains/{brain_id}/documents",
dependencies=[Depends(AuthBearer()), Depends(has_brain_authorization())],

View File

@ -32,6 +32,7 @@ class IntegrationDescriptionEntity(BaseModel):
max_files: int
allow_model_change: bool
integration_display_name: str
onboarding_brain: bool
class IntegrationEntity(BaseModel):

View File

@ -22,7 +22,6 @@ from modules.brain.repository.interfaces.external_api_secrets_interface import (
)
from modules.brain.service.api_brain_definition_service import ApiBrainDefinitionService
from modules.brain.service.brain_service import BrainService
from modules.user.entity.user_identity import UserIdentity
logger = get_logger(__name__)
@ -74,33 +73,6 @@ class BrainUserService:
brain_id=brain_id,
)
def get_default_user_brain_or_create_new(self, user: UserIdentity):
default_brain = self.get_user_default_brain(user.id)
if not default_brain:
default_brain = brain_service.create_brain(brain=None, user_id=user.id)
self.brain_user_repository.create_brain_user(
user.id, default_brain.brain_id, RoleEnum.Owner, True
)
return default_brain
def set_as_default_brain_for_user(self, user_id: UUID, brain_id: UUID):
old_default_brain = self.get_user_default_brain(user_id)
if old_default_brain is not None:
self.brain_user_repository.update_brain_user_default_status(
user_id=user_id,
brain_id=old_default_brain.brain_id,
default_brain=False,
)
self.brain_user_repository.update_brain_user_default_status(
user_id=user_id,
brain_id=brain_id,
default_brain=True,
)
def delete_brain_users(self, brain_id: UUID) -> None:
self.brain_user_repository.delete_brain_subscribers(
brain_id=brain_id,

View File

@ -5,22 +5,6 @@ from modules.brain.service.brain_user_service import BrainUserService
brain_user_service = BrainUserService()
def test_retrieve_default_brain(client, api_key):
# Making a GET request to the /brains/default/ endpoint
response = client.get(
"/brains/default/",
headers={"Authorization": "Bearer " + api_key},
)
# Assert that the response status code is 200 (HTTP OK)
assert response.status_code == 200
default_brain = response.json()
assert "id" in default_brain
assert "name" in default_brain
def test_create_brain(client, api_key):
# Generate a random name for the brain
random_brain_name = "".join(

View File

@ -39,8 +39,7 @@ 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, fetchPublicPrompts } = useBrainContext();
const { onClickOutside } = useOutsideClickListener();
const { session } = useSupabase();
@ -49,7 +48,7 @@ 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();

View File

@ -4,6 +4,7 @@
@use "@/styles/Spacings.module.scss";
@use "@/styles/Typography.module.scss";
@use "@/styles/Variables.module.scss";
@use "@/styles/ZIndexes.module.scss";
.main_container {
position: relative;
@ -71,3 +72,21 @@
}
}
}
.onboarding_overlay {
width: 100%;
height: 100%;
z-index: ZIndexes.$overlay;
background-color: var(--background-blur);
position: absolute;
top: 0;
left: 0;
.first_brain_button {
position: absolute;
right: Spacings.$spacing07;
top: Spacings.$spacing04;
display: flex;
gap: Spacings.$spacing05;
}
}

View File

@ -8,9 +8,12 @@ import { useBrainCreationContext } from "@/lib/components/AddBrainModal/brainCre
import { OnboardingModal } from "@/lib/components/OnboardingModal/OnboardingModal";
import PageHeader from "@/lib/components/PageHeader/PageHeader";
import { UploadDocumentModal } from "@/lib/components/UploadDocumentModal/UploadDocumentModal";
import { MessageInfoBox } from "@/lib/components/ui/MessageInfoBox/MessageInfoBox";
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
import { SearchBar } from "@/lib/components/ui/SearchBar/SearchBar";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useUserSettingsContext } from "@/lib/context/UserSettingsProvider/hooks/useUserSettingsContext";
import { useUserData } from "@/lib/hooks/useUserData";
import { redirectToLogin } from "@/lib/router/redirectToLogin";
import { ButtonType } from "@/lib/types/QuivrButton";
@ -19,7 +22,9 @@ import styles from "./page.module.scss";
const Search = (): JSX.Element => {
const pathname = usePathname();
const { session } = useSupabase();
const { setIsBrainCreationModalOpened } = useBrainCreationContext();
const { isBrainCreationModalOpened, setIsBrainCreationModalOpened } =
useBrainCreationContext();
const { userIdentityData } = useUserData();
const { isDarkMode } = useUserSettingsContext();
useEffect(() => {
@ -40,6 +45,7 @@ const Search = (): JSX.Element => {
];
return (
<>
<div className={styles.main_container}>
<div className={styles.page_header}>
<PageHeader iconName="home" label="Home" buttons={buttons} />
@ -69,6 +75,24 @@ const Search = (): JSX.Element => {
<AddBrainModal />
<OnboardingModal />
</div>
{!isBrainCreationModalOpened && !userIdentityData?.onboarded && (
<div className={styles.onboarding_overlay}>
<div className={styles.first_brain_button}>
<MessageInfoBox type="tutorial">
<span>Press the following button to create your first brain</span>
</MessageInfoBox>
<QuivrButton
iconName="brain"
label="Create Brain"
color="primary"
onClick={() => {
setIsBrainCreationModalOpened(true);
}}
/>
</div>
</div>
)}
</>
);
};

View File

@ -3,7 +3,6 @@
import { useCallback, useEffect } from "react";
import { useFormContext } from "react-hook-form";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { Brain } from "@/lib/context/BrainProvider/types";
import { useUrlBrain } from "@/lib/hooks/useBrainIdFromUrl";
import { BrainConfig, Model } from "@/lib/types/BrainConfig";
@ -14,8 +13,6 @@ import { useBrainFetcher } from "../../../hooks/useBrainFetcher";
export const useBrainFormState = () => {
const { brainId } = useUrlBrain();
const { defaultBrainId } = useBrainContext();
const {
register,
getValues,
@ -30,7 +27,6 @@ export const useBrainFormState = () => {
brainId,
});
const isDefaultBrain = defaultBrainId === brainId;
const promptId = watch("prompt_id");
const openAiKey = watch("openAiKey");
const model = watch("model");
@ -80,7 +76,6 @@ export const useBrainFormState = () => {
model,
temperature,
maxTokens,
isDefaultBrain,
promptId,
openAiKey,
defaultValues,

View File

@ -23,14 +23,13 @@ type UseSettingsTabProps = {
export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
const { t } = useTranslation(["translation", "brain", "config"]);
const [isUpdating, setIsUpdating] = useState(false);
const [isSettingAsDefault, setIsSettingAsDefault] = useState(false);
const { publish } = useToast();
const formRef = useRef<HTMLFormElement>(null);
const { setAsDefaultBrain, updateBrain } = useBrainApi();
const { fetchAllBrains, fetchDefaultBrain } = useBrainContext();
const { updateBrain } = useBrainApi();
const { fetchAllBrains } = useBrainContext();
const { userData } = useUserData();
const { getValues, maxTokens, setValue, openAiKey, model, isDefaultBrain } =
const { getValues, maxTokens, setValue, openAiKey, model } =
useBrainFormState();
const accessibleModels = getAccessibleModels({
@ -57,38 +56,6 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
};
}, [formRef.current]);
const setAsDefaultBrainHandler = async () => {
try {
setIsSettingAsDefault(true);
await setAsDefaultBrain(brainId);
publish({
variant: "success",
text: t("defaultBrainSet", { ns: "config" }),
});
void fetchAllBrains();
void fetchDefaultBrain();
} catch (err) {
// ...
if (isAxiosError(err) && err.response?.status === 429) {
publish({
variant: "danger",
text: `${JSON.stringify(
(
err.response as {
data: { detail: string };
}
).data.detail
)}`,
});
return;
}
} finally {
setIsSettingAsDefault(false);
}
};
const handleSubmit = async () => {
const { name, description } = getValues();
@ -142,10 +109,7 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
return {
handleSubmit,
setAsDefaultBrainHandler,
isUpdating,
isSettingAsDefault,
isDefaultBrain,
formRef,
accessibleModels,
setIsUpdating,

View File

@ -51,15 +51,6 @@ export const deleteBrain = async (
await axiosInstance.delete(`/brains/${brainId}/subscription`);
};
export const getDefaultBrain = async (
axiosInstance: AxiosInstance
): Promise<MinimalBrainForUser | undefined> => {
return mapBackendMinimalBrainToMinimalBrain(
(await axiosInstance.get<BackendMinimalBrainForUser>(`/brains/default/`))
.data
);
};
export const getBrains = async (
axiosInstance: AxiosInstance
): Promise<MinimalBrainForUser[]> => {
@ -113,13 +104,6 @@ export const updateBrainAccess = async (
});
};
export const setAsDefaultBrain = async (
brainId: string,
axiosInstance: AxiosInstance
): Promise<void> => {
await axiosInstance.post(`/brains/${brainId}/default`);
};
export const updateBrain = async (
brainId: string,
brain: UpdateBrainInput,

View File

@ -92,6 +92,7 @@ export type IntegrationBrains = {
tags: IntegrationBrainTag[];
information: string;
integration_display_name: string;
onboarding_brain: boolean;
};
export type UpdateBrainInput = Partial<CreateBrainInput>;

View File

@ -7,11 +7,9 @@ import {
getBrain,
getBrains,
getBrainUsers,
getDefaultBrain,
getDocsFromQuestion,
getIntegrationBrains,
getPublicBrains,
setAsDefaultBrain,
Subscription,
updateBrain,
updateBrainAccess,
@ -31,7 +29,6 @@ export const useBrainApi = () => {
createBrain: async (brain: CreateBrainInput) =>
createBrain(brain, axiosInstance),
deleteBrain: async (id: string) => deleteBrain(id, axiosInstance),
getDefaultBrain: async () => getDefaultBrain(axiosInstance),
getBrains: async () => getBrains(axiosInstance),
getBrain: async (id: string) => getBrain(id, axiosInstance),
addBrainSubscriptions: async (
@ -45,8 +42,6 @@ export const useBrainApi = () => {
userEmail: string,
subscription: SubscriptionUpdatableProperties
) => updateBrainAccess(brainId, userEmail, subscription, axiosInstance),
setAsDefaultBrain: async (brainId: string) =>
setAsDefaultBrain(brainId, axiosInstance),
updateBrain: async (brainId: string, brain: UpdateBrainInput) =>
updateBrain(brainId, brain, axiosInstance),
getPublicBrains: async () => getPublicBrains(axiosInstance),

View File

@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
import { Modal } from "@/lib/components/ui/Modal/Modal";
import { addBrainDefaultValues } from "@/lib/config/defaultBrainConfig";
import { useUserData } from "@/lib/hooks/useUserData";
import styles from "./AddBrainModal.module.scss";
import { useBrainCreationContext } from "./brainCreation-provider";
@ -15,6 +16,7 @@ import { CreateBrainProps } from "./types/types";
export const AddBrainModal = (): JSX.Element => {
const { t } = useTranslation(["translation", "brain", "config"]);
const { userIdentityData } = useUserData();
const {
isBrainCreationModalOpened,
@ -43,6 +45,7 @@ export const AddBrainModal = (): JSX.Element => {
desc={t("newBrainSubtitle", { ns: "brain" })}
isOpen={isBrainCreationModalOpened}
setOpen={setIsBrainCreationModalOpened}
unclosable={!userIdentityData?.onboarded}
size="big"
CloseTrigger={<div />}
>

View File

@ -24,6 +24,11 @@
flex-direction: column;
gap: Spacings.$spacing02;
&.disabled {
pointer-events: none;
opacity: 0.1;
}
.tag_wrapper {
height: 2rem;
}

View File

@ -6,6 +6,7 @@ import { MessageInfoBox } from "@/lib/components/ui/MessageInfoBox/MessageInfoBo
import { Tag } from "@/lib/components/ui/Tag/Tag";
import Tooltip from "@/lib/components/ui/Tooltip/Tooltip";
import { useUserSettingsContext } from "@/lib/context/UserSettingsProvider/hooks/useUserSettingsContext";
import { useUserData } from "@/lib/hooks/useUserData";
import styles from "./BrainCatalogue.module.scss";
@ -21,6 +22,7 @@ export const BrainCatalogue = ({
const { setCurrentSelectedBrain, currentSelectedBrain } =
useBrainCreationContext();
const { isDarkMode } = useUserSettingsContext();
const { userIdentityData } = useUserData();
return (
<div className={styles.cards_wrapper}>
@ -30,13 +32,26 @@ export const BrainCatalogue = ({
use cases or data sources.
</span>
</MessageInfoBox>
{!userIdentityData?.onboarded && (
<MessageInfoBox type="tutorial">
<span>
Let&apos;s start by creating a Docs &amp; URLs brain.<br></br>Of
course, feel free to explore other types of brains during your Quivr
journey.
</span>
</MessageInfoBox>
)}
<span className={styles.title}>Choose a brain type</span>
<div className={styles.brains_grid}>
{brains.map((brain) => {
return (
<div
key={brain.id}
className={styles.brain_card_container}
className={`${styles.brain_card_container} ${
!userIdentityData?.onboarded && !brain.onboarding_brain
? styles.disabled
: ""
}`}
onClick={() => {
next();
setCurrentSelectedBrain(brain);

View File

@ -2,9 +2,12 @@ import { capitalCase } from "change-case";
import { useEffect, useState } from "react";
import { KnowledgeToFeed } from "@/app/chat/[chatId]/components/ActionsBar/components";
import { useUserApi } from "@/lib/api/user/useUserApi";
import { MessageInfoBox } from "@/lib/components/ui/MessageInfoBox/MessageInfoBox";
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
import { TextInput } from "@/lib/components/ui/TextInput/TextInput";
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
import { useUserData } from "@/lib/hooks/useUserData";
import styles from "./CreateBrainStep.module.scss";
import { useBrainCreationApi } from "./hooks/useBrainCreationApi";
@ -18,6 +21,9 @@ export const CreateBrainStep = (): JSX.Element => {
const { creating, setCreating, currentSelectedBrain } =
useBrainCreationContext();
const [createBrainStepIndex, setCreateBrainStepIndex] = useState<number>(0);
const { knowledgeToFeed } = useKnowledgeToFeedContext();
const { userIdentityData } = useUserData();
const { updateUserIdentity } = useUserApi();
useEffect(() => {
if (currentSelectedBrain?.connection_settings) {
@ -42,19 +48,21 @@ export const CreateBrainStep = (): JSX.Element => {
goToPreviousStep();
};
const feed = (): void => {
const feed = async (): Promise<void> => {
if (!userIdentityData?.onboarded) {
await updateUserIdentity({
...userIdentityData,
username: userIdentityData?.username ?? "",
onboarded: true,
});
}
setCreating(true);
createBrain();
};
if (currentStepIndex !== 2) {
return <></>;
}
const renderSettings = () => {
return (
<div className={styles.brain_knowledge_wrapper}>
{!createBrainStepIndex && (
<div className={styles.settings_wrapper}>
<>
<MessageInfoBox type="warning">
{currentSelectedBrain?.information}
</MessageInfoBox>
@ -62,23 +70,34 @@ export const CreateBrainStep = (): JSX.Element => {
<TextInput
key={name}
inputValue={value}
setInputValue={(inputValue) =>
handleInputChange(name, inputValue)
}
setInputValue={(inputValue) => handleInputChange(name, inputValue)}
label={capitalCase(name)}
/>
))}
</div>
</>
);
};
const renderFeedBrain = () => {
return (
<>
{!userIdentityData?.onboarded && (
<MessageInfoBox type="tutorial">
<span>
Upload documents or add URLs to add knowledges to your brain.
</span>
</MessageInfoBox>
)}
{!!currentSelectedBrain?.max_files && !!createBrainStepIndex && (
<div>
<span className={styles.title}>Feed your brain</span>
<KnowledgeToFeed hideBrainSelector={true} />
</div>
)}
{!currentSelectedBrain?.max_files &&
!currentSelectedBrain?.connection_settings && (
<div className={styles.message_info_box_wrapper}>
</>
);
};
const renderCreateButton = () => {
return (
<MessageInfoBox type="info">
<div className={styles.message_content}>
Click on
@ -92,9 +111,11 @@ export const CreateBrainStep = (): JSX.Element => {
to finish your brain creation.
</div>
</MessageInfoBox>
</div>
)}
);
};
const renderButtons = () => {
return (
<div className={styles.buttons_wrapper}>
<QuivrButton
label="Previous step"
@ -109,6 +130,9 @@ export const CreateBrainStep = (): JSX.Element => {
color="primary"
iconName="add"
onClick={feed}
disabled={
knowledgeToFeed.length === 0 && !userIdentityData?.onboarded
}
isLoading={creating}
/>
) : (
@ -121,6 +145,24 @@ export const CreateBrainStep = (): JSX.Element => {
/>
)}
</div>
);
};
if (currentStepIndex !== 2) {
return <></>;
}
return (
<div className={styles.brain_knowledge_wrapper}>
{!createBrainStepIndex && renderSettings()}
{!!currentSelectedBrain?.max_files &&
!!createBrainStepIndex &&
renderFeedBrain()}
{!currentSelectedBrain?.max_files &&
!currentSelectedBrain?.connection_settings &&
renderCreateButton()}
{renderButtons()}
</div>
);
};

View File

@ -46,7 +46,7 @@ export const OnboardingModal = (): JSX.Element => {
await updateUserIdentity({
username: methods.getValues("username"),
company: methods.getValues("companyName"),
onboarded: true,
onboarded: false,
company_size: methods.getValues("companySize"),
usage_purpose: methods.getValues("usagePurpose") as
| UsagePurpose

View File

@ -29,6 +29,12 @@
background-color: var(--warning-lightest);
}
&.tutorial {
border-color: var(--gold);
color: var(--gold);
background-color: var(--gold);
}
&.dark {
background-color: var(--background-special-0);
}

View File

@ -8,7 +8,7 @@ import { Icon } from "../Icon/Icon";
export type MessageInfoBoxProps = {
children: React.ReactNode;
type: "info" | "success" | "warning" | "error";
type: "info" | "success" | "warning" | "error" | "tutorial";
unforceWhite?: boolean;
};
@ -28,6 +28,8 @@ export const MessageInfoBox = ({
return { iconName: "check", iconColor: "success" };
case "warning":
return { iconName: "warning", iconColor: "warning" };
case "tutorial":
return { iconName: "step", iconColor: "gold" };
default:
return { iconName: "info", iconColor: "primary" };
}

View File

@ -29,8 +29,8 @@ export const QuivrButton = ({
${isDarkMode ? styles.dark : ""}
${hidden ? styles.hidden : ""}
`}
// eslint-disable-next-line @typescript-eslint/no-misused-promises, @typescript-eslint/prefer-optional-chain, @typescript-eslint/no-unnecessary-condition
onClick={() => onClick && onClick()}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick={() => onClick?.()}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>

View File

@ -67,7 +67,7 @@ export const SearchBar = ({
message={message}
setMessage={setMessage}
onSubmit={() => void submit()}
placeholder="Search"
placeholder="Ask a question..."
></Editor>
{searching ? (
<LoaderIcon size="big" color="accent" />

View File

@ -17,14 +17,12 @@ import { MinimalBrainForUser } from "../types";
export const useBrainProvider = () => {
const { publish } = useToast();
const { track } = useEventTracking();
const { createBrain, deleteBrain, getBrains, getDefaultBrain } =
useBrainApi();
const { createBrain, deleteBrain, getBrains } = useBrainApi();
const { getPublicPrompts } = usePromptApi();
const { t } = useTranslation(["delete_or_unsubscribe_from_brain"]);
const [allBrains, setAllBrains] = useState<MinimalBrainForUser[]>([]);
const [currentBrainId, setCurrentBrainId] = useState<null | UUID>(null);
const [defaultBrainId, setDefaultBrainId] = useState<UUID>();
const [isFetchingBrains, setIsFetchingBrains] = useState(true);
const [publicPrompts, setPublicPrompts] = useState<Prompt[]>([]);
const [currentPromptId, setCurrentPromptId] = useState<null | string>(null);
@ -87,13 +85,6 @@ export const useBrainProvider = () => {
[deleteBrain, publish, track]
);
const fetchDefaultBrain = useCallback(async () => {
const userDefaultBrain = await getDefaultBrain();
if (userDefaultBrain !== undefined) {
setDefaultBrainId(userDefaultBrain.id);
}
}, [currentBrainId, getDefaultBrain]);
const fetchPublicPrompts = useCallback(async () => {
setPublicPrompts(await getPublicPrompts());
}, [getPublicPrompts]);
@ -108,9 +99,6 @@ export const useBrainProvider = () => {
currentBrainId,
setCurrentBrainId,
defaultBrainId,
fetchDefaultBrain,
fetchPublicPrompts,
publicPrompts,
currentPrompt,

View File

@ -21,7 +21,7 @@ export const OnboardingProvider = ({
useEffect(() => {
if (userIdentityData) {
setIsOnboardingModalOpened(!userIdentityData.onboarded);
setIsOnboardingModalOpened(!userIdentityData.username);
}
}, [userIdentityData]);

View File

@ -38,6 +38,7 @@ import {
import {
IoArrowUpCircleOutline,
IoCloudDownloadOutline,
IoFootsteps,
IoHomeOutline,
IoSettingsSharp,
IoShareSocial,
@ -123,6 +124,7 @@ export const iconList: { [name: string]: IconType } = {
share: IoShareSocial,
software: CgSoftwareDownload,
star: FaRegStar,
step: IoFootsteps,
sun: FaSun,
thumbsDown: FaRegThumbsDown,
thumbsUp: FaRegThumbsUp,

View File

@ -0,0 +1,3 @@
alter table "public"."integrations" add column "onboarding_brain" boolean default false;