ai-paywall-notification.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. import { useEffect } from 'react'
  2. import Notification from '@/shared/components/notification'
  3. import PaywallUpgradeButton from '@/shared/components/paywall-upgrade-button'
  4. import { useEditorContext } from '@/shared/context/editor-context'
  5. import { useUserFeaturesContext } from '@/shared/context/user-features-context'
  6. import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
  7. import { useTranslation } from 'react-i18next'
  8. import { formatSecondsToHoursAndMinutes } from '@/shared/utils/time'
  9. import { useFeatureFlag } from '@/shared/context/split-test-context'
  10. import getMeta from '@/utils/meta'
  11. const hasUnlimitedAi = getMeta('ol-hasUnlimitedAi')
  12. type AiFeatureLocations = 'errorAssist' | 'workbench'
  13. type PaywallType = 'assistant' | 'workbench'
  14. const paywallTypeByLocation: Record<AiFeatureLocations, PaywallType> = {
  15. workbench: 'workbench',
  16. errorAssist: 'assistant',
  17. }
  18. function AiPaywallNotification({
  19. isActionBelowContent = false,
  20. featureLocation,
  21. }: {
  22. isActionBelowContent?: boolean
  23. featureLocation: AiFeatureLocations
  24. }) {
  25. const {
  26. hasSuggestionsLeft,
  27. hasTokensLeft,
  28. tokenResetDate,
  29. premiumSuggestionResetDate,
  30. } = useEditorContext()
  31. const { t } = useTranslation()
  32. const features = useUserFeaturesContext()
  33. const inQuotaRollout = useFeatureFlag('plans-2026-phase-1')
  34. const user = getMeta('ol-user')
  35. const isCommons = user.hasInstitutionLicence
  36. const isGroupUser = user.isMemberOfGroupSubscription
  37. if (!getMeta('ol-showAiFeatures')) {
  38. return null
  39. }
  40. // todo: quota clean-up: remove once we are transitioned off aiErrorAssistant naming and replace with just hasUnlimitedAi, also remove null FF check
  41. const hasAddOn = hasUnlimitedAi || Boolean(features?.aiErrorAssistant)
  42. // error assist only needs usage quota
  43. const canUseErrorAssist = hasSuggestionsLeft
  44. if (canUseErrorAssist && featureLocation === 'errorAssist') {
  45. return null
  46. }
  47. // workbench needs both tokens and usage quota
  48. const canUseWorkbench = hasSuggestionsLeft && hasTokensLeft
  49. if (canUseWorkbench && featureLocation === 'workbench') {
  50. return null
  51. }
  52. const exceededQuotaDates = [
  53. ...(hasSuggestionsLeft ? [] : [premiumSuggestionResetDate]),
  54. ...(hasTokensLeft ? [] : [tokenResetDate]),
  55. ]
  56. const longestResetDate = exceededQuotaDates.reduce((latest, date) =>
  57. date > latest ? date : latest
  58. )
  59. const secondsTillReset =
  60. (longestResetDate.getTime() - new Date().getTime()) / 1000
  61. // if we should have refreshed already remove paywall
  62. if (secondsTillReset <= 0) {
  63. return null
  64. }
  65. // if not in rollout, use old paywalls for free users
  66. if (!inQuotaRollout && !hasAddOn) {
  67. return null
  68. }
  69. if (!inQuotaRollout) {
  70. // if not in rollout, we can still use our fair usage messages
  71. const chattingMessage = t(
  72. 'youve_reached_the_fair_usage_limit_on_your_plan_you_can_start_chatting_again_in_time',
  73. {
  74. time: formatSecondsToHoursAndMinutes(t, secondsTillReset),
  75. }
  76. )
  77. const assistMessage = t('youve_reached_the_fair_usage')
  78. return (
  79. <Notification
  80. type="info"
  81. title={t('usage_limit_reached')}
  82. content={
  83. featureLocation === 'workbench' ? chattingMessage : assistMessage
  84. }
  85. isDismissible={false}
  86. customIcon={null}
  87. className="ai-paywall-notification"
  88. />
  89. )
  90. }
  91. if (hasAddOn) {
  92. return (
  93. <FairUseLimit
  94. secondsTillReset={secondsTillReset}
  95. featureLocation={featureLocation}
  96. />
  97. )
  98. }
  99. if (isGroupUser) {
  100. return (
  101. <GroupsPaywall
  102. secondsTillReset={secondsTillReset}
  103. featureLocation={featureLocation}
  104. />
  105. )
  106. }
  107. if (isCommons) {
  108. return (
  109. <CommonsPaywall
  110. secondsTillReset={secondsTillReset}
  111. featureLocation={featureLocation}
  112. />
  113. )
  114. }
  115. return (
  116. <UpgradePaywall
  117. secondsTillReset={secondsTillReset}
  118. isActionBelowContent={isActionBelowContent}
  119. featureLocation={featureLocation}
  120. />
  121. )
  122. }
  123. function GroupsPaywall({
  124. secondsTillReset,
  125. featureLocation,
  126. }: {
  127. secondsTillReset: number
  128. featureLocation: AiFeatureLocations
  129. }) {
  130. const { t } = useTranslation()
  131. const message = t('your_limit_will_reset_in_time_or_speak_to_admin', {
  132. time: formatSecondsToHoursAndMinutes(t, secondsTillReset),
  133. })
  134. const title =
  135. featureLocation === 'workbench'
  136. ? t('youve_reached_your_daily_ai_limit')
  137. : t('youve_hit_your_daily_ai_limit')
  138. return (
  139. <>
  140. <Notification
  141. type="info"
  142. title={title}
  143. content={message}
  144. isDismissible={false}
  145. customIcon={null}
  146. className="ai-paywall-notification"
  147. />
  148. </>
  149. )
  150. }
  151. function CommonsPaywall({
  152. secondsTillReset,
  153. featureLocation,
  154. }: {
  155. secondsTillReset: number
  156. featureLocation: AiFeatureLocations
  157. }) {
  158. const { t } = useTranslation()
  159. // workbench isnt available on commons plans, so dont show it here and let upgrade-notification handle it
  160. if (featureLocation === 'workbench') {
  161. return null
  162. }
  163. const message = t('this_will_reset_in', {
  164. time: formatSecondsToHoursAndMinutes(t, secondsTillReset),
  165. })
  166. return (
  167. <>
  168. <Notification
  169. type="info"
  170. title={t('youve_reached_your_ai_usage_limit')}
  171. content={message}
  172. isDismissible={false}
  173. customIcon={null}
  174. className="ai-paywall-notification"
  175. />
  176. </>
  177. )
  178. }
  179. function FairUseLimit({
  180. secondsTillReset,
  181. featureLocation,
  182. }: {
  183. secondsTillReset: number
  184. featureLocation: AiFeatureLocations
  185. }) {
  186. const { t } = useTranslation()
  187. const title =
  188. featureLocation === 'workbench'
  189. ? t('usage_limit_reached')
  190. : t('youve_reached_the_fair_usage')
  191. const workbenchMessage = t(
  192. 'youve_reached_the_fair_usage_limit_on_your_plan_you_can_start_chatting_again_in_time',
  193. {
  194. time: formatSecondsToHoursAndMinutes(t, secondsTillReset),
  195. }
  196. )
  197. const assistMessage = t('this_will_reset_in', {
  198. time: formatSecondsToHoursAndMinutes(t, secondsTillReset),
  199. })
  200. const message =
  201. featureLocation === 'workbench' ? workbenchMessage : assistMessage
  202. return (
  203. <>
  204. <Notification
  205. type="info"
  206. title={title}
  207. content={message}
  208. isDismissible={false}
  209. customIcon={null}
  210. className="ai-paywall-notification"
  211. />
  212. </>
  213. )
  214. }
  215. function UpgradePaywall({
  216. secondsTillReset,
  217. isActionBelowContent,
  218. featureLocation,
  219. }: {
  220. secondsTillReset: number
  221. isActionBelowContent: boolean
  222. featureLocation: AiFeatureLocations
  223. }) {
  224. const { t } = useTranslation()
  225. const { sendEvent } = useEditorAnalytics()
  226. const paywallType = paywallTypeByLocation[featureLocation]
  227. useEffect(() => {
  228. sendEvent('paywall-prompt', {
  229. 'paywall-type': paywallType,
  230. })
  231. }, [sendEvent, paywallType])
  232. return (
  233. <Notification
  234. type="info"
  235. title={t('youve_hit_your_daily_ai_limit')}
  236. content={t('upgrade_for_unlimited_access_to_ai', {
  237. time: formatSecondsToHoursAndMinutes(t, secondsTillReset),
  238. })}
  239. isDismissible={false}
  240. customIcon={null}
  241. isActionBelowContent={isActionBelowContent}
  242. action={
  243. <PaywallUpgradeButton
  244. referrer="ai"
  245. paywallType={paywallType}
  246. className="px-2.5 py-2"
  247. />
  248. }
  249. className="ai-upgrade-paywall-btn ai-paywall-notification"
  250. />
  251. )
  252. }
  253. export default AiPaywallNotification