ai-paywall-notification.tsx 6.3 KB

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