From a598c552a593652af43502227c5f86ca290098de Mon Sep 17 00:00:00 2001 From: Malik Glossop Date: Fri, 20 Mar 2026 09:08:31 +0100 Subject: [PATCH] Merge pull request #31737 from overleaf/mg-shared-ai-consent Migrate Writefull AI consent to shared tutorial system GitOrigin-RevId: c9298a177c9f1aa1a941c96599d6d854591f8a76 --- .../web/frontend/extracted-translations.json | 6 +- .../components/ai-consent-prompt-message.tsx | 59 +++++++++++++++++++ .../js/shared/hooks/use-ai-consent.ts | 33 +++++++++++ services/web/locales/en.json | 6 +- .../frontend/features/workbench/chat.spec.tsx | 6 +- .../frontend/helpers/editor-providers.tsx | 20 ------- .../helpers/make-tutorial-provider.tsx | 32 ++++++++++ .../shared/hooks/use-tutorial.spec.tsx | 6 +- 8 files changed, 132 insertions(+), 36 deletions(-) create mode 100644 services/web/frontend/js/shared/components/ai-consent-prompt-message.tsx create mode 100644 services/web/frontend/js/shared/hooks/use-ai-consent.ts create mode 100644 services/web/test/frontend/helpers/make-tutorial-provider.tsx diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 5137807719..322d0df083 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -219,7 +219,6 @@ "buy_licenses": "", "buy_more_licenses": "", "buy_now_no_exclamation_mark": "", - "by_continuing_you_agree_to_use_this_feature_in_line_with_your_institutions_policies": "", "by_email": "", "by_sharing_link": "", "by_subscribing_you_agree_to_our_terms_of_service": "", @@ -284,7 +283,6 @@ "choose_how_you_search_your_references": "", "choose_which_experiments": "", "citation": "", - "citation_checks_are_powered_by_trusted_sources_from_dimensions": "", "cite_a_reference_from_your_reference_manager_to_automatically_add_it_to_your_bib_file": "", "cite_directly_or_import_references": "", "cite_faster": "", @@ -756,6 +754,7 @@ "github_workflow_files_delete_github_repo": "", "github_workflow_files_error": "", "github_workflow_files_error_non_owner": "", + "give-ai-consent": "", "give_feedback": "", "go_next_page": "", "go_page": "", @@ -1839,7 +1838,6 @@ "suggestion_applied": "", "suggests_code_completions_while_typing": "", "support_for_your_browser_is_ending_soon": "", - "supporting_your_research_respecting_your_privacy": "", "supports_up_to_x_licenses": "", "sure_you_want_to_cancel_plan_change": "", "sure_you_want_to_change_plan": "", @@ -2318,7 +2316,6 @@ "you_have_x_licenses_and_your_plan_supports_up_to_y": "", "you_have_x_licenses_on_your_subscription": "", "you_need_to_configure_your_sso_settings": "", - "you_remain_in_control_please_review_all_suggestions_carefully": "", "you_unpaused_your_subscription": "", "you_will_be_able_to_reassign_subscription": "", "youll_get_best_results_in_visual_but_can_be_used_in_source": "", @@ -2355,7 +2352,6 @@ "your_plan_is_limited_to_n_editors": "", "your_plan_is_limited_to_n_editors_plural": "", "your_premium_plan_is_paused": "", - "your_project_content_is_never_used_for_model_training": "", "your_project_exceeded_collaborator_limit": "", "your_project_exceeded_compile_timeout_limit_on_free_plan": "", "your_project_near_compile_timeout_limit": "", diff --git a/services/web/frontend/js/shared/components/ai-consent-prompt-message.tsx b/services/web/frontend/js/shared/components/ai-consent-prompt-message.tsx new file mode 100644 index 0000000000..50ecc4f4b2 --- /dev/null +++ b/services/web/frontend/js/shared/components/ai-consent-prompt-message.tsx @@ -0,0 +1,59 @@ +import React from 'react' + +interface ConsentPromptMessageBodyProps { + className?: string + listClassName?: string +} + +interface ConsentPromptMessageHeaderProps { + className?: string +} + +export const AiConsentPromptMessageHeader = ({ + className, +}: ConsentPromptMessageHeaderProps) => { + return ( +
+ Supporting your research and protecting your privacy +
+ ) +} + +export const AiConsentPromptMessageBody = ({ + className, + listClassName, +}: ConsentPromptMessageBodyProps) => { + return ( +
+ +

+ By using these features, you approve the use and sharing of your data in + this way. +

+
+ ) +} diff --git a/services/web/frontend/js/shared/hooks/use-ai-consent.ts b/services/web/frontend/js/shared/hooks/use-ai-consent.ts new file mode 100644 index 0000000000..e890232f4f --- /dev/null +++ b/services/web/frontend/js/shared/hooks/use-ai-consent.ts @@ -0,0 +1,33 @@ +import { useCallback, useMemo, useState } from 'react' +import useTutorial from '@/shared/hooks/promotions/use-tutorial' + +const AI_CONSENT_TUTORIAL_KEY = 'workbench-consent-release' +const eventData = { name: AI_CONSENT_TUTORIAL_KEY } + +export { AI_CONSENT_TUTORIAL_KEY } + +export default function useAiConsent() { + const { completeTutorial, checkCompletion } = useTutorial( + AI_CONSENT_TUTORIAL_KEY, + eventData + ) + + const hasGivenAiConsent = useMemo(() => checkCompletion(), [checkCompletion]) + const [consentError, setConsentError] = useState(false) + + const giveAiConsent = useCallback(async () => { + setConsentError(false) + try { + await completeTutorial( + { event: 'promo-click', action: 'complete' }, + { failSilently: false } + ) + return true + } catch { + setConsentError(true) + return false + } + }, [completeTutorial]) + + return { hasGivenAiConsent, giveAiConsent, consentError } +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 14a7aec202..5f5160044f 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -277,7 +277,6 @@ "buy_more_licenses": "Buy more licenses", "buy_now_no_exclamation_mark": "Buy now", "by": "by", - "by_continuing_you_agree_to_use_this_feature_in_line_with_your_institutions_policies": "By continuing, you agree to use this feature in line with your institution’s policies.", "by_email": "By email", "by_joining_labs": "By joining Labs, you agree to receive occasional emails and updates from Overleaf—for example, to request your feedback. You also agree to our <0>terms of service and <1>privacy notice.", "by_registering_you_agree_to_our_terms_of_service": "By registering, you agree to our <0>terms of service and <1>privacy notice.", @@ -356,7 +355,6 @@ "choose_which_experiments": "Choose which experiments you’d like to try.", "choose_your_plan": "Choose your plan", "citation": "Citation", - "citation_checks_are_powered_by_trusted_sources_from_dimensions": "Citation checks are powered by trusted sources from Dimensions.", "cite_a_reference_from_your_reference_manager_to_automatically_add_it_to_your_bib_file": "Cite a reference from your reference manager to automatically add it to your .bib file.", "cite_directly_or_import_references": "Cite directly or import references", "cite_faster": "Cite faster", @@ -963,6 +961,7 @@ "github_workflow_files_delete_github_repo": "The repository has been created on GitHub but linking was unsuccessful. You will have to delete GitHub repository or choose a new name.", "github_workflow_files_error": "The __appName__ GitHub sync service couldn’t sync GitHub Workflow files (in .github/workflows/). Please authorize __appName__ to edit your GitHub workflow files and try again.", "github_workflow_files_error_non_owner": "The __appName__ GitHub sync service couldn’t sync GitHub Workflow files (in .github/workflows/). Please ensure the project owner has authorized __appName__ to edit this project’s GitHub workflow files and try again.", + "give-ai-consent": "Give AI consent", "give_feedback": "Give feedback", "global": "global", "go_back_and_link_accts": "Go back and link your accounts", @@ -2343,7 +2342,6 @@ "suggests_code_completions_while_typing": "Suggests code completions while typing", "support": "Support", "support_for_your_browser_is_ending_soon": "Support for your browser is ending soon", - "supporting_your_research_respecting_your_privacy": "Supporting your research, respecting your privacy", "supports_up_to_x_licenses": "Supports up to <0>__count__ licenses", "sure_you_want_to_cancel_plan_change": "Are you sure you want to revert your scheduled plan change? You will remain subscribed to the <0>__planName__ plan.", "sure_you_want_to_change_plan": "Are you sure you want to change plan to <0>__planName__?", @@ -2891,7 +2889,6 @@ "you_have_x_licenses_and_your_plan_supports_up_to_y": "You have allocated __addedUsersSize__ licenses and your plan supports up to __groupSize__.", "you_have_x_licenses_on_your_subscription": "You have __groupSize__ licenses on your subscription.", "you_need_to_configure_your_sso_settings": "You need to configure and test your SSO settings before enabling SSO", - "you_remain_in_control_please_review_all_suggestions_carefully": "You remain in control, so please review all suggestions carefully before accepting them.", "you_unpaused_your_subscription": "You unpaused your subscription.", "you_will_be_able_to_contact_us_any_time_to_share_your_feedback": "<0>You will be able to contact us any time to share your feedback", "you_will_be_able_to_reassign_subscription": "You will be able to reassign their subscription membership to another person in your organization", @@ -2932,7 +2929,6 @@ "your_plan_is_limited_to_n_editors": "Your plan allows __count__ collaborator with edit access and unlimited viewers.", "your_plan_is_limited_to_n_editors_plural": "Your plan allows __count__ collaborators with edit access and unlimited viewers.", "your_premium_plan_is_paused": "Your Premium plan is <0>paused.", - "your_project_content_is_never_used_for_model_training": "Your project content is never used for model training. There are more details about how we use your data in <0>our docs.", "your_project_exceeded_collaborator_limit": "Your project exceeded the collaborator limit and access levels were changed. Select a new access level for your collaborators, or upgrade to add more editors or reviewers.", "your_project_exceeded_compile_timeout_limit_on_free_plan": "Your project exceeded the compile timeout limit on our free plan.", "your_project_near_compile_timeout_limit": "Your project is near the compile timeout limit for our free plan.", diff --git a/services/web/test/frontend/features/workbench/chat.spec.tsx b/services/web/test/frontend/features/workbench/chat.spec.tsx index 3f9ae43f3f..5c95bd29d1 100644 --- a/services/web/test/frontend/features/workbench/chat.spec.tsx +++ b/services/web/test/frontend/features/workbench/chat.spec.tsx @@ -1,4 +1,4 @@ -import { consentTutorialKey } from '@modules/workbench/frontend/js/components/chat' +import { AI_CONSENT_TUTORIAL_KEY } from '@/shared/hooks/use-ai-consent' import Workbench from '@modules/workbench/frontend/js/components/workbench' import { EditorProviders } from '../../helpers/editor-providers' import { EditorView } from '@codemirror/view' @@ -13,7 +13,9 @@ describe('Workbench', { scrollBehavior: false }, function () { win.metaAttributesCache.set('ol-splitTestVariants', { 'ai-workbench-release': 'enabled', }) - win.metaAttributesCache.set('ol-inactiveTutorials', [consentTutorialKey]) + win.metaAttributesCache.set('ol-inactiveTutorials', [ + AI_CONSENT_TUTORIAL_KEY, + ]) }) }) diff --git a/services/web/test/frontend/helpers/editor-providers.tsx b/services/web/test/frontend/helpers/editor-providers.tsx index 6c3da56ae0..a3186c4ad5 100644 --- a/services/web/test/frontend/helpers/editor-providers.tsx +++ b/services/web/test/frontend/helpers/editor-providers.tsx @@ -55,7 +55,6 @@ import { DetachCompileContext } from '@/shared/context/detach-compile-context' import { type CompileContext } from '@/shared/context/local-compile-context' import { EditorContext } from '@/shared/context/editor-context' import { Cobranding } from '@ol-types/cobranding' -import { TutorialContext } from '@/shared/context/tutorial-context' import { EDITOR_SESSION_ID } from '@/features/pdf-preview/util/metrics' // these constants can be imported in tests instead of @@ -311,25 +310,6 @@ export function makeEditorProvider({ return EditorProvider } -export const makeTutorialProvider = (opts?: { - inactiveTutorials: string[] -}) => { - const TutorialProvider: FC = ({ children }) => { - const value = { - deactivateTutorial: () => {}, - inactiveTutorials: opts?.inactiveTutorials ?? [], - currentPopup: null, - setCurrentPopup: () => {}, - } - return ( - - {children} - - ) - } - return TutorialProvider -} - const makeReferencesProvider = () => { const ReferencesProvider: FC = ({ children }) => { return ( diff --git a/services/web/test/frontend/helpers/make-tutorial-provider.tsx b/services/web/test/frontend/helpers/make-tutorial-provider.tsx new file mode 100644 index 0000000000..01dff51702 --- /dev/null +++ b/services/web/test/frontend/helpers/make-tutorial-provider.tsx @@ -0,0 +1,32 @@ +import React, { + type FC, + type PropsWithChildren, + useCallback, + useState, +} from 'react' +import { TutorialContext } from '@/shared/context/tutorial-context' + +export const makeTutorialProvider = (opts?: { + inactiveTutorials: string[] +}) => { + const TutorialProvider: FC = ({ children }) => { + const [inactiveTutorials, setInactiveTutorials] = useState( + opts?.inactiveTutorials ?? [] + ) + const deactivateTutorial = useCallback((key: string) => { + setInactiveTutorials(prev => (prev.includes(key) ? prev : [...prev, key])) + }, []) + const value = { + deactivateTutorial, + inactiveTutorials, + currentPopup: null, + setCurrentPopup: () => {}, + } + return ( + + {children} + + ) + } + return TutorialProvider +} diff --git a/services/web/test/frontend/shared/hooks/use-tutorial.spec.tsx b/services/web/test/frontend/shared/hooks/use-tutorial.spec.tsx index 698441aff1..21329bef06 100644 --- a/services/web/test/frontend/shared/hooks/use-tutorial.spec.tsx +++ b/services/web/test/frontend/shared/hooks/use-tutorial.spec.tsx @@ -1,9 +1,7 @@ import useTutorial from '@/shared/hooks/promotions/use-tutorial' import { useEffect, useState } from 'react' -import { - EditorProviders, - makeTutorialProvider, -} from '../../helpers/editor-providers' +import { EditorProviders } from '../../helpers/editor-providers' +import { makeTutorialProvider } from '../../helpers/make-tutorial-provider' const TutorialTester = ({ tutorial,