From 811e935ced02fb0fdb919195a5fb3677f37ccfd5 Mon Sep 17 00:00:00 2001 From: Jimmy Domagala-Tang Date: Mon, 4 Nov 2024 08:44:40 -0800 Subject: [PATCH] Merge pull request #21107 from overleaf/jdt-error-assistant-freemium Convert Ai Error Assistant to a Freemium Model GitOrigin-RevId: 348c19262928d7dde8236baf37663c85d91f101a --- .../src/Features/Project/ProjectController.js | 22 +++++++++++++-- .../web/frontend/extracted-translations.json | 6 ++++ .../pdf-preview/hooks/use-log-events.ts | 23 +++++++++++---- .../js/shared/context/editor-context.tsx | 28 ++++++++++++++++++- services/web/locales/en.json | 5 ++++ services/web/types/user.ts | 8 ++++++ 6 files changed, 84 insertions(+), 8 deletions(-) diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index 23f7e09df2..9910bf1c50 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -348,6 +348,9 @@ const _ProjectController = { 'write-and-cite-ars', 'default-visual-for-beginners', 'hotjar', + 'spell-check-client', + 'spell-check-no-server', + 'ai-add-on', ].filter(Boolean) const getUserValues = async userId => @@ -646,6 +649,21 @@ const _ProjectController = { aiFeaturesAllowed = false } } + const canUseErrorAssistant = + user.features?.aiErrorAssistant || + splitTestAssignments['ai-add-on']?.variant === 'enabled' + + let featureUsage = {} + + if (Features.hasFeature('saas')) { + const usagesLeft = await Modules.promises.hooks.fire( + 'remainingFeatureAllocation', + userId + ) + usagesLeft?.forEach(usage => { + featureUsage = { ...featureUsage, ...usage } + }) + } // check if a user has never tried writefull before (writefull.enabled will be null) // if they previously accepted writefull, or are have been already assigned to a trial, user.writefull will be true, @@ -713,6 +731,7 @@ const _ProjectController = { allowedFreeTrial, featureSwitches: user.featureSwitches, features: user.features, + featureUsage, refProviders: _.mapValues(user.refProviders, Boolean), writefull: { enabled: Boolean(user.writefull?.enabled && aiFeaturesAllowed), @@ -766,8 +785,7 @@ const _ProjectController = { showSymbolPalette, symbolPaletteAvailable: Features.hasFeature('symbol-palette'), userRestrictions: Array.from(req.userRestrictions || []), - showAiErrorAssistant: - aiFeaturesAllowed && user.features?.aiErrorAssistant, + showAiErrorAssistant: aiFeaturesAllowed && canUseErrorAssistant, detachRole, metadata: { viewport: false }, showUpgradePrompt, diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 19fb69a49e..7fa4b1aaaa 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -152,6 +152,8 @@ "browser": "", "bulk_accept_confirm": "", "bulk_reject_confirm": "", + "buy_error_assistant_for": "", + "buy_now_no_exclamation_mark": "", "buy_overleaf_assist": "", "by_subscribing_you_agree_to_our_terms_of_service": "", "can_edit": "", @@ -483,6 +485,7 @@ "first_name": "", "fit_to_height": "", "fit_to_width": "", + "fix_latex_errors_fast_and_perfect_your_projects": "", "fixed_width": "", "fixed_width_wrap_text": "", "fold_line": "", @@ -525,6 +528,7 @@ "get_discounted_plan": "", "get_dropbox_sync": "", "get_early_access_to_ai": "", + "get_error_assistant": "", "get_exclusive_access_to_labs": "", "get_full_project_history": "", "get_git_integration": "", @@ -763,6 +767,7 @@ "last_used": "", "latam_discount_modal_info": "", "latam_discount_modal_title": "", + "latex_error_fixing_with_a_little_help": "", "latex_in_thirty_minutes": "", "latex_places_figures_according_to_a_special_algorithm": "", "latex_places_tables_according_to_a_special_algorithm": "", @@ -1068,6 +1073,7 @@ "premium_feature": "", "premium_features": "", "premium_plan_label": "", + "premium_suggestion_available": "", "presentation_mode": "", "press_and_awards": "", "previous_page": "", diff --git a/services/web/frontend/js/features/pdf-preview/hooks/use-log-events.ts b/services/web/frontend/js/features/pdf-preview/hooks/use-log-events.ts index 7953da3933..d849786026 100644 --- a/services/web/frontend/js/features/pdf-preview/hooks/use-log-events.ts +++ b/services/web/frontend/js/features/pdf-preview/hooks/use-log-events.ts @@ -31,11 +31,24 @@ export const useLogEvents = (setShowLogs: (show: boolean) => void) => { }) if (suggestFix) { - element - .querySelector( - 'button[data-action="suggest-fix"]' - ) - ?.click() + // if they are paywalled, click that instead + const paywall = document.querySelector( + 'button[data-action="assistant-paywall-show"]' + ) + + if (paywall) { + paywall.scrollIntoView({ + block: 'start', + inline: 'nearest', + }) + paywall.click() + } else { + element + .querySelector( + 'button[data-action="suggest-fix"]' + ) + ?.click() + } } } }) diff --git a/services/web/frontend/js/shared/context/editor-context.tsx b/services/web/frontend/js/shared/context/editor-context.tsx index 4b4e701f3e..9d414a0fef 100644 --- a/services/web/frontend/js/shared/context/editor-context.tsx +++ b/services/web/frontend/js/shared/context/editor-context.tsx @@ -49,13 +49,17 @@ export const EditorContext = createContext< currentPopup: string | null setCurrentPopup: Dispatch> setOutOfSync: (value: boolean) => void + hasPremiumSuggestion: boolean + setHasPremiumSuggestion: (value: boolean) => void + setPremiumSuggestionResetDate: (date: Date) => void + premiumSuggestionResetDate: Date } | undefined >(undefined) export const EditorProvider: FC = ({ children }) => { const ide = useIdeContext() - const { id: userId } = useUserContext() + const { id: userId, featureUsage } = useUserContext() const { role } = useDetachContext() const { showGenericMessageModal } = useModalsContext() @@ -91,6 +95,20 @@ export const EditorProvider: FC = ({ children }) => { ) const [currentPopup, setCurrentPopup] = useState(null) + const [hasPremiumSuggestion, setHasPremiumSuggestion] = useState( + () => { + return Boolean( + featureUsage?.aiErrorAssistant && + featureUsage?.aiErrorAssistant.remainingUsage > 0 + ) + } + ) + const [premiumSuggestionResetDate, setPremiumSuggestionResetDate] = + useState(() => { + return featureUsage?.aiErrorAssistant.resetDate + ? new Date(featureUsage.aiErrorAssistant.resetDate) + : new Date() + }) const isPendingEditor = useMemo( () => @@ -183,6 +201,10 @@ export const EditorProvider: FC = ({ children }) => { currentPopup, setCurrentPopup, setOutOfSync, + hasPremiumSuggestion, + setHasPremiumSuggestion, + premiumSuggestionResetDate, + setPremiumSuggestionResetDate, }), [ cobranding, @@ -203,6 +225,10 @@ export const EditorProvider: FC = ({ children }) => { setCurrentPopup, outOfSync, setOutOfSync, + hasPremiumSuggestion, + setHasPremiumSuggestion, + premiumSuggestionResetDate, + setPremiumSuggestionResetDate, ] ) diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 5da1234a42..11b9055b4f 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -208,6 +208,7 @@ "built_in": "Built-In", "bulk_accept_confirm": "Are you sure you want to accept the selected __nChanges__ changes?", "bulk_reject_confirm": "Are you sure you want to reject the selected __nChanges__ changes?", + "buy_error_assistant_for": "Buy Error Assist for $__aiAddonCost__ per year", "buy_now_no_exclamation_mark": "Buy now", "buy_overleaf_assist": "Buy Overleaf Assist", "by": "by", @@ -690,6 +691,7 @@ "first_name_sentence_case": "First name", "fit_to_height": "Fit to height", "fit_to_width": "Fit to width", + "fix_latex_errors_fast_and_perfect_your_projects": "Fix LaTeX errors fast and perfect your projects with the AI-powered Error Assist.", "fixed_width": "Fixed width", "fixed_width_wrap_text": "Fixed width, wrap text", "flexible_plans_for_everyone": "Flexible plans for everyone—from individual students and researchers, to large businesses and universities.", @@ -762,6 +764,7 @@ "get_discounted_plan": "Get discounted plan", "get_dropbox_sync": "Get Dropbox Sync", "get_early_access_to_ai": "Get early access to the new AI Error Assistant in Overleaf Labs", + "get_error_assistant": "Get Error Assistant", "get_exclusive_access_to_labs": "Get exclusive access to early-stage experiments when you join Overleaf Labs. All we ask in return is your honest feedback to help us develop and improve.", "get_full_project_history": "Get full project history", "get_git_integration": "Get Git integration", @@ -1089,6 +1092,7 @@ "latam_discount_offer_plans_page_banner": "__flag__ We’ve applied a __discount__ discount to premium plans on this page for our users in __country__. Check out the new lower prices (in __currency__).", "latex_articles_page_summary": "Papers, presentations, reports and more, written in LaTeX and published by our community. Search or browse below.", "latex_articles_page_title": "Articles - Papers, Presentations, Reports and more", + "latex_error_fixing_with_a_little_help": "LaTeX error fixing with a little help from AI", "latex_examples": "LaTeX examples", "latex_examples_page_summary": "Examples of powerful LaTeX packages and techniques in use — a great way to learn LaTeX by example. Search or browse below.", "latex_examples_page_title": "Examples - Equations, Formatting, TikZ, Packages and More", @@ -1549,6 +1553,7 @@ "premium_feature": "Premium feature", "premium_features": "Premium features", "premium_plan_label": "You’re using Overleaf Premium", + "premium_suggestion_available": "Premium suggestion available", "presentation": "Presentation", "presentation_mode": "Presentation mode", "press_and_awards": "Press & awards", diff --git a/services/web/types/user.ts b/services/web/types/user.ts index 1ecd6cdb2e..c8714f8a24 100644 --- a/services/web/types/user.ts +++ b/services/web/types/user.ts @@ -25,6 +25,13 @@ export type Features = { zotero?: boolean } +export type FeatureUsage = { + [feature: string]: { + remainingUsage: number + resetDate: string // date string + } +} + export type User = { id: UserId | null isAdmin?: boolean @@ -44,6 +51,7 @@ export type User = { autoCreatedAccount: boolean firstAutoLoad: boolean } + featureUsage?: FeatureUsage } export type MongoUser = Pick> & { _id: string }