From fda96b2fdf38b0b2ef8fb3dbd814ed81bbbf1eea Mon Sep 17 00:00:00 2001 From: Domagoj Kriskovic Date: Tue, 24 Jun 2025 11:29:53 +0200 Subject: [PATCH] Add promo notifications for AI assist (#26514) * Add promo notifications for AI assist * format pug GitOrigin-RevId: 8895145e1e5dcd8e28f29bf2601a4bd21456a407 --- .../Project/ProjectListController.mjs | 24 +++- services/web/app/views/project/list-react.pug | 5 + .../web/frontend/extracted-translations.json | 6 + .../notifications/ai-assist-banner.tsx | 128 ++++++++++++++++++ .../components/notifications/notification.tsx | 9 +- .../notifications/user-notifications.tsx | 2 + .../js/shared/components/notification.tsx | 21 ++- .../js/shared/svgs/sparkle-2-stars.svg | 16 +++ services/web/frontend/js/utils/meta.ts | 1 + .../bootstrap-5/components/notifications.scss | 6 + services/web/locales/en.json | 6 + .../components/project-list-root.test.tsx | 6 +- .../Project/ProjectListController.test.mjs | 25 ++++ 13 files changed, 248 insertions(+), 7 deletions(-) create mode 100644 services/web/frontend/js/features/project-list/components/notifications/ai-assist-banner.tsx create mode 100644 services/web/frontend/js/shared/svgs/sparkle-2-stars.svg diff --git a/services/web/app/src/Features/Project/ProjectListController.mjs b/services/web/app/src/Features/Project/ProjectListController.mjs index ab2b0e3082..b12d1c3cc9 100644 --- a/services/web/app/src/Features/Project/ProjectListController.mjs +++ b/services/web/app/src/Features/Project/ProjectListController.mjs @@ -27,6 +27,8 @@ import SplitTestHandler from '../SplitTests/SplitTestHandler.js' import SplitTestSessionHandler from '../SplitTests/SplitTestSessionHandler.js' import TutorialHandler from '../Tutorial/TutorialHandler.js' import SubscriptionHelper from '../Subscription/SubscriptionHelper.js' +import PermissionsManager from '../Authorization/PermissionsManager.js' +import SubscriptionLocator from '../Subscription/SubscriptionLocator.js' /** * @import { GetProjectsRequest, GetProjectsResponse, AllUsersProjects, MongoProject } from "./types" @@ -117,7 +119,7 @@ async function projectListPage(req, res, next) { const user = await User.findById( userId, `email emails features alphaProgram betaProgram lastPrimaryEmailCheck signUpDate refProviders${ - isSaas ? ' enrollment writefull completedTutorials' : '' + isSaas ? ' enrollment writefull completedTutorials aiErrorAssistant' : '' }` ) @@ -409,6 +411,25 @@ async function projectListPage(req, res, next) { 'papers-notification-banner' ) + await SplitTestHandler.promises.getAssignment( + req, + res, + 'ai-assist-notification' + ) + + const canUseAi = await PermissionsManager.promises.checkUserPermissions( + user, + ['use-ai'] + ) + const assistantDisabled = user.aiErrorAssistant?.enabled === false // the assistant has been manually disabled by the user + const subscription = + await SubscriptionLocator.promises.getUsersSubscription(userId) + const hasManuallyCollectedSubscription = + subscription?.collectionMethod === 'manual' + const canUseAiAssist = + canUseAi && !hasManuallyCollectedSubscription && !assistantDisabled + const hasAiAssist = user.features?.aiErrorAssistant + const customerIoEnabled = await SplitTestHandler.promises.hasUserBeenAssignedToVariant( req, @@ -450,6 +471,7 @@ async function projectListPage(req, res, next) { hasIndividualPaidSubscription, userRestrictions: Array.from(req.userRestrictions || []), customerIoEnabled, + showAiAssistNotification: canUseAiAssist && !hasAiAssist, }) } diff --git a/services/web/app/views/project/list-react.pug b/services/web/app/views/project/list-react.pug index fa7bc24c09..78103e75a6 100644 --- a/services/web/app/views/project/list-react.pug +++ b/services/web/app/views/project/list-react.pug @@ -86,6 +86,11 @@ block append meta data-type='string' content=usGovBannerVariant ) + meta( + name='ol-showAiAssistNotification' + data-type='boolean' + content=showAiAssistNotification + ) block content #project-list-root diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 156ec9df3f..b404e428a5 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -420,6 +420,8 @@ "disconnected": "", "discount": "", "discount_of": "", + "discover_research_writing_toolkit": "", + "discover_research_writing_toolkit_description": "", "discover_the_fastest_way_to_search_and_cite": "", "display": "", "display_deleted_user": "", @@ -2098,9 +2100,13 @@ "work_in_vim_or_emacs_emulation_mode": "", "work_offline": "", "work_offline_pull_to_overleaf": "", + "work_smarter_with_ai_assist": "", + "work_smarter_with_ai_assist_description": "", "work_with_non_overleaf_users": "", "work_with_other_github_users": "", "write_faster_smarter_with_overleaf_and_writefull_ai_tools": "", + "write_smarter_right_now": "", + "write_smarter_right_now_description": "", "writefull": "", "writefull_loading_error_body": "", "writefull_loading_error_title": "", diff --git a/services/web/frontend/js/features/project-list/components/notifications/ai-assist-banner.tsx b/services/web/frontend/js/features/project-list/components/notifications/ai-assist-banner.tsx new file mode 100644 index 0000000000..dd6d0a0e97 --- /dev/null +++ b/services/web/frontend/js/features/project-list/components/notifications/ai-assist-banner.tsx @@ -0,0 +1,128 @@ +import { memo, useCallback, useEffect } from 'react' +import Notification from './notification' +import { useTranslation } from 'react-i18next' +import OLButton from '@/features/ui/components/ol/ol-button' +import { sendMB } from '@/infrastructure/event-tracking' +import sparkle from '@/shared/svgs/sparkle-2-stars.svg' +import { useSplitTest } from '@/shared/context/split-test-context' +import { useProjectListContext } from '../../context/project-list-context' +import getMeta from '@/utils/meta' +import usePersistedState from '@/shared/hooks/use-persisted-state' + +function AiAssistBanner() { + const { title, description, cta } = useTitleDescription() + const { totalProjectsCount } = useProjectListContext() + const [dismissed, setDismissed] = usePersistedState( + 'ai-assist-notification-banner-dismissed', + false + ) + const { variant } = useSplitTest('ai-assist-notification') + const { t } = useTranslation() + + useEffect(() => { + if (!dismissed) { + sendMB('promo-prompt', { + location: 'dashboard-banner', + name: 'ai-assist', + variant, + }) + } + }, [dismissed, variant]) + + const handleClose = useCallback(() => { + sendMB('promo-dismiss', { + location: 'dashboard-banner', + name: 'ai-assist', + variant, + }) + setDismissed(true) + }, [setDismissed, variant]) + + const handleUpgradeClick = useCallback(() => { + sendMB('promo-click', { + location: 'dashboard-banner', + name: 'ai-assist', + variant, + type: 'click-upgrade', + }) + }, [variant]) + + const handleLearnMoreClick = useCallback(() => { + sendMB('promo-click', { + location: 'dashboard-banner', + name: 'ai-assist', + type: 'click-learn-more', + variant, + }) + }, [variant]) + + if ( + !title || + dismissed || + totalProjectsCount === 0 || + !getMeta('ol-showAiAssistNotification') + ) { + return null + } + + return ( +