Files
Verso/services/web/frontend/js/shared/components/ai-paywall-notification.tsx
T
Jimmy Domagala-TangandCopybot bb5d90a332 Add usage quota to Workbench (#31782)
* feat: adding usage rate limiting to workbench and aligning editor context values for suggestionsLeft

* feat: prepend word token to headers of token rate limiter to prevent confusion with usage rate limiter

* Shared AI paywalls (#31948)

* feat: renaming hasPremiumSuggestion and adding token limits to editor context and project load

* feat: adding new ai features paywall component

* feat: rename getRemainingFeatureUses to token based naming for token based limiter, removed checking for feature usage on anonymous users, and removed guard on null userId since we shouldnt be calling getRemainingFeatureUses on a nonexistent user

* feat: using token rate limit headers to set token rate values in editor context

* feat: update workbench to be available without refreshing if rate limit reset occurs within session

* fix: move paywall out of inert section

* Hide new paywalls behind FF and open plans page on upgrade attempt (#32023)

* feat: hide new paywalls behind FF

* feat: update ai paywall buttons to navigate to plans page post quota plans change release

* feat: showing a fair limit notificaiton pre-quota change, and updating paywall to not fire if user has premium already (#32056)

GitOrigin-RevId: 565fb128d55543fea34c383bc4abeaa3dd148d09
2026-03-06 09:17:52 +00:00

167 lines
4.5 KiB
TypeScript

import Notification from '@/shared/components/notification'
import UpgradeButton from '@/features/ide-react/components/toolbar/upgrade-button'
import { useEditorContext } from '@/shared/context/editor-context'
import { useUserFeaturesContext } from '@/shared/context/user-features-context'
import { useTranslation } from 'react-i18next'
import { formatSecondsToHoursAndMinutes } from '@/shared/utils/time'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import getMeta from '@/utils/meta'
const onAiFreeTrial = getMeta('ol-onAiFreeTrial')
type aiFeatureLocations = 'errorAssist' | 'workbench'
function AiPaywallNotification({
isActionBelowContent = false,
featureLocation,
}: {
isActionBelowContent?: boolean
featureLocation: aiFeatureLocations
}) {
const {
hasSuggestionsLeft,
hasTokensLeft,
tokenResetDate,
premiumSuggestionResetDate,
} = useEditorContext()
const { t } = useTranslation()
const features = useUserFeaturesContext()
const inQuotaRollout = useFeatureFlag('plans-2026-phase-1')
if (!getMeta('ol-showAiFeatures')) {
return null
}
// todo: quota clean-up: remove once we are transitioned off aiErrorAssistant naming and replace with just !onAiFreeTrial, also remove null FF check
const hasAddOn = !onAiFreeTrial || Boolean(features?.aiErrorAssistant)
// error assist only needs usage quota
const canUseErrorAssist = hasSuggestionsLeft
if (canUseErrorAssist && featureLocation === 'errorAssist') {
return null
}
// workbench needs both tokens and usage quota
const canUseWorkbench = hasSuggestionsLeft && hasTokensLeft
if (canUseWorkbench && featureLocation === 'workbench') {
return null
}
const exceededQuotaDates = [
...(hasSuggestionsLeft ? [] : [premiumSuggestionResetDate]),
...(hasTokensLeft ? [] : [tokenResetDate]),
]
const longestResetDate = exceededQuotaDates.reduce((latest, date) =>
date > latest ? date : latest
)
const secondsTillReset =
(longestResetDate.getTime() - new Date().getTime()) / 1000
// if we should have refreshed already remove paywall
if (secondsTillReset <= 0) {
return null
}
// if not in rollout, use old paywalls for free users
if (!inQuotaRollout && !hasAddOn) {
return null
}
if (!inQuotaRollout) {
// if not in rollout, we can still use our fair usage messages
const chattingMessage = t(
'youve_reached_the_fair_usage_limit_on_your_plan_you_can_start_chatting_again_in_time',
{
time: formatSecondsToHoursAndMinutes(t, secondsTillReset),
}
)
const assistMessage = t('youve_reached_the_ai_fair_usage')
return (
<Notification
type="info"
title={t('usage_limit_reached')}
content={
featureLocation === 'workbench' ? chattingMessage : assistMessage
}
isDismissible={false}
/>
)
}
if (hasAddOn) {
return (
<FairUseLimit
secondsTillReset={secondsTillReset}
featureLocation={featureLocation}
/>
)
}
const message = t('upgrade_for_unlimited_access_to_ai', {
time: formatSecondsToHoursAndMinutes(t, secondsTillReset),
})
return (
<>
<Notification
type="info"
title={t('youve_hit_your_overleaf_ai_limit')}
content={message}
isDismissible={false}
customIcon={null}
isActionBelowContent={isActionBelowContent}
action={
<UpgradeButton
className="px-2.5 py-2"
referrer="ai"
source={featureLocation}
/>
}
className="ai-upgrade-paywall-btn"
/>
</>
)
}
function FairUseLimit({
secondsTillReset,
featureLocation,
}: {
secondsTillReset: number
featureLocation: aiFeatureLocations
}) {
const { t } = useTranslation()
const title =
featureLocation === 'workbench'
? t('usage_limit_reached')
: t('youve_reached_the_ai_fair_usage')
const workbenchMessage = t(
'youve_reached_the_fair_usage_limit_on_your_plan_you_can_start_chatting_again_in_time',
{
time: formatSecondsToHoursAndMinutes(t, secondsTillReset),
}
)
const assistMessage = t('this_will_reset_in', {
time: formatSecondsToHoursAndMinutes(t, secondsTillReset),
})
const message =
featureLocation === 'workbench' ? workbenchMessage : assistMessage
return (
<>
<Notification
type="info"
title={title}
content={message}
isDismissible={false}
/>
</>
)
}
export default AiPaywallNotification