Parcourir la source

Merge pull request #21107 from overleaf/jdt-error-assistant-freemium

Convert Ai Error Assistant to a Freemium Model

GitOrigin-RevId: 348c19262928d7dde8236baf37663c85d91f101a
Jimmy Domagala-Tang il y a 1 an
Parent
commit
811e935ced

+ 20 - 2
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,

+ 6 - 0
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": "",

+ 18 - 5
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<HTMLButtonElement>(
-                'button[data-action="suggest-fix"]'
-              )
-              ?.click()
+            // if they are paywalled, click that instead
+            const paywall = document.querySelector<HTMLButtonElement>(
+              'button[data-action="assistant-paywall-show"]'
+            )
+
+            if (paywall) {
+              paywall.scrollIntoView({
+                block: 'start',
+                inline: 'nearest',
+              })
+              paywall.click()
+            } else {
+              element
+                .querySelector<HTMLButtonElement>(
+                  'button[data-action="suggest-fix"]'
+                )
+                ?.click()
+            }
           }
         }
       })

+ 27 - 1
services/web/frontend/js/shared/context/editor-context.tsx

@@ -49,13 +49,17 @@ export const EditorContext = createContext<
       currentPopup: string | null
       setCurrentPopup: Dispatch<SetStateAction<string | null>>
       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<string | null>(null)
+  const [hasPremiumSuggestion, setHasPremiumSuggestion] = useState<boolean>(
+    () => {
+      return Boolean(
+        featureUsage?.aiErrorAssistant &&
+          featureUsage?.aiErrorAssistant.remainingUsage > 0
+      )
+    }
+  )
+  const [premiumSuggestionResetDate, setPremiumSuggestionResetDate] =
+    useState<Date>(() => {
+      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,
     ]
   )
 

+ 5 - 0
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": "<strong>Buy Error Assist for $__aiAddonCost__ per year</strong>",
   "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 <b>Overleaf Premium</b>",
+  "premium_suggestion_available": "Premium suggestion available",
   "presentation": "Presentation",
   "presentation_mode": "Presentation mode",
   "press_and_awards": "Press & awards",

+ 8 - 0
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<User, Exclude<keyof User, 'id'>> & { _id: string }