diff --git a/src/components/Disclosure/Disclosure.scss b/src/components/Disclosure/Disclosure.scss index a9a6f623..d65a5f44 100644 --- a/src/components/Disclosure/Disclosure.scss +++ b/src/components/Disclosure/Disclosure.scss @@ -6,6 +6,8 @@ background-color: variables.$dark-green; font-family: Inter, Arial, sans-serif; padding: 2rem; + display: flex; + flex-direction: column; h2 { font-size: 1.375rem; @@ -44,7 +46,6 @@ .disclaimer { font-size: 0.75rem; - margin-bottom: 7.5rem; } .trial-period { @@ -75,7 +76,15 @@ .trial-upgrade { background: #D74000; border-radius: 99rem; - font-size: 0.875rem; + font-size: 0 + .875rem; + } + + .pgn__form-group { + margin: 0; + } + .pgn__form-control-decorator-group { + margin: 0; } } diff --git a/src/components/Disclosure/index.jsx b/src/components/Disclosure/index.jsx index 4be1b43a..8f5d916a 100644 --- a/src/components/Disclosure/index.jsx +++ b/src/components/Disclosure/index.jsx @@ -6,67 +6,82 @@ import { QuestionAnswerOutline, LightbulbCircle, AutoAwesome } from '@openedx/pa import { ensureConfig, getConfig } from '@edx/frontend-platform/config'; import './Disclosure.scss'; +import { useCourseUpgrade, useTrackEvent } from '../../hooks'; ensureConfig(['PRIVACY_POLICY_URL']); -const Disclosure = ({ children, showTrial }) => ( -
-

- Xpert Learning Assistant -

-

An AI-powered educational tool

-
-
- -
- Understand a concept
- “How does photosynthesis work?” -
-
-
- -
- Summarize your learning
- “Can you help me review pivot tables?” -
-
-
- {showTrial ? ( -
-
-
- - - Free trial, then upgrade course for full access to Xpert features. - +const Disclosure = ({ children }) => { + const { upgradeable, upgradeUrl, auditTrialLengthDays } = useCourseUpgrade(); + const { track } = useTrackEvent(); + + const handleClick = () => track('edx.ui.lms.learning_assistant.message'); + const freeDays = auditTrialLengthDays === 1 ? '1 day' : `${auditTrialLengthDays} days`; + + return ( +
+

+ Xpert Learning Assistant +

+
+

An AI-powered educational tool

+
+
+ +
+ Understand a concept
+ “How does photosynthesis work?” +
+
+
+ +
+ Summarize your learning
+ “Can you help me review pivot tables?” +
-
+ {upgradeable ? ( +
+
+
+ + + Free for {freeDays}, then upgrade course for full access to Xpert features. + +
+ +
+
+ ) : null} +

+ Note: This chat is AI generated, mistakes are possible. + By using it you agree that edX may create a record of this chat. + Your personal data will be used as described in our   + + privacy policy + + . +

- ) : null} -

- Note: This chat is AI generated, mistakes are possible. - By using it you agree that edX may create a record of this chat. - Your personal data will be used as described in our   - - privacy policy - - . -

- {children} -
-); +
+ {children} +
+ + ); +}; Disclosure.propTypes = { - showTrial: PropTypes.bool, children: PropTypes.node.isRequired, }; -Disclosure.defaultProps = { - showTrial: false, -}; - export default Disclosure; diff --git a/src/components/MessageForm/index.jsx b/src/components/MessageForm/index.jsx index b86353bd..69c3c23f 100644 --- a/src/components/MessageForm/index.jsx +++ b/src/components/MessageForm/index.jsx @@ -57,7 +57,7 @@ const MessageForm = ({ courseId, shouldAutofocus, unitId }) => { ); return ( -
+ - {disclosureAcknowledged ? (getSidebar()) : ({getMessageForm()})} + {disclosureAcknowledged + ? (getSidebar()) + : ({getMessageForm()})}
) ); diff --git a/src/context/course-info-context.js b/src/context/course-info-context.js new file mode 100644 index 00000000..107dd0df --- /dev/null +++ b/src/context/course-info-context.js @@ -0,0 +1,9 @@ +import { createContext } from 'react'; + +export const CourseInfoContext = createContext('course-info', { + courseId: null, + unitId: null, + isUpgradeEligible: false, +}); + +export const CourseInfoProvider = CourseInfoContext.Provider; diff --git a/src/context/index.js b/src/context/index.js new file mode 100644 index 00000000..dca05b8c --- /dev/null +++ b/src/context/index.js @@ -0,0 +1 @@ +export * from './course-info-context'; diff --git a/src/data/slice.js b/src/data/slice.js index 91982422..fc64c47e 100644 --- a/src/data/slice.js +++ b/src/data/slice.js @@ -12,6 +12,7 @@ export const initialState = { sidebarIsOpen: false, isEnabled: false, auditTrial: {}, + auditTrialLengthDays: null, }; export const learningAssistantSlice = createSlice({ @@ -48,6 +49,9 @@ export const learningAssistantSlice = createSlice({ setAuditTrial: (state, { payload }) => { state.auditTrial = payload; }, + setAuditTrialLengthDays: (state, { payload }) => { + state.auditTrialLengthDays = payload; + }, }, }); @@ -62,6 +66,7 @@ export const { setSidebarIsOpen, setIsEnabled, setAuditTrial, + setAuditTrialLengthDays, } = learningAssistantSlice.actions; export const { diff --git a/src/data/thunks.js b/src/data/thunks.js index 2bf2528a..31d71844 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -17,6 +17,7 @@ import { setSidebarIsOpen, setIsEnabled, setAuditTrial, + setAuditTrialLengthDays, } from './slice'; import { OPTIMIZELY_PROMPT_EXPERIMENT_KEY } from './optimizely'; @@ -134,6 +135,8 @@ export function getLearningAssistantChatSummary(courseId) { if (Object.keys(auditTrial).length !== 0) { dispatch(setAuditTrial(auditTrial)); } + + if (data.audit_trial_length_days) { dispatch(setAuditTrialLengthDays(data.audit_trial_length_days)); } } catch (error) { dispatch(setApiError()); } diff --git a/src/hooks/index.js b/src/hooks/index.js new file mode 100644 index 00000000..22f1a63c --- /dev/null +++ b/src/hooks/index.js @@ -0,0 +1,3 @@ +/* eslint-disable import/prefer-default-export */ +export { default as useCourseUpgrade } from './use-course-upgrade'; +export { default as useTrackEvent } from './use-track-event'; diff --git a/src/hooks/use-course-upgrade.js b/src/hooks/use-course-upgrade.js new file mode 100644 index 00000000..f5544890 --- /dev/null +++ b/src/hooks/use-course-upgrade.js @@ -0,0 +1,39 @@ +import { useContext } from 'react'; +import { useModel } from '@src/generic/model-store'; // eslint-disable-line import/no-unresolved +import { useSelector } from 'react-redux'; +import { CourseInfoContext } from '../context'; + +const millisecondsInOneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds + +export default function useCourseUpgrade() { + const { courseId, isUpgradeEligible } = useContext(CourseInfoContext); + const { offer } = useModel('coursewareMeta', courseId); + const { verifiedMode } = useModel('courseHomeMeta', courseId); + const { + auditTrialLengthDays, + auditTrial, + } = useSelector(state => state.learningAssistant); + + const upgradeUrl = offer?.upgradeUrl || verifiedMode?.upgradeUrl; + + if (!isUpgradeEligible || !upgradeUrl) { return { upgradeable: false }; } + + let auditTrialExpired = false; + let auditTrialDaysRemaining; + + if (auditTrial?.expirationDate) { + const auditTrialExpirationDate = new Date(auditTrial.expirationDate); + auditTrialDaysRemaining = Math.ceil((auditTrialExpirationDate - Date.now()) / millisecondsInOneDay); + + auditTrialExpired = auditTrialDaysRemaining < 0; + } + + return { + upgradeable: true, + auditTrialLengthDays, + auditTrialDaysRemaining, + auditTrialExpired, + auditTrial, + upgradeUrl, + }; +} diff --git a/src/hooks/use-track-event.js b/src/hooks/use-track-event.js new file mode 100644 index 00000000..7205326c --- /dev/null +++ b/src/hooks/use-track-event.js @@ -0,0 +1,20 @@ +import { useContext } from 'react'; +import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; +import { sendTrackEvent } from '@edx/frontend-platform/analytics'; +import { CourseInfoContext } from '../context'; + +export default function useTrackEvent() { + const { courseId, moduleId } = useContext(CourseInfoContext); + const { userId } = getAuthenticatedUser(); + + const track = (event, details) => { + sendTrackEvent(event, { + course_id: courseId, + user_id: userId, + module_id: moduleId, + ...details, + }); + }; + + return { track }; +} diff --git a/src/widgets/Xpert.jsx b/src/widgets/Xpert.jsx index a8a6148a..48cc8283 100644 --- a/src/widgets/Xpert.jsx +++ b/src/widgets/Xpert.jsx @@ -1,19 +1,24 @@ import PropTypes from 'prop-types'; -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { updateSidebarIsOpen, getLearningAssistantChatSummary } from '../data/thunks'; import ToggleXpert from '../components/ToggleXpertButton'; import Sidebar from '../components/Sidebar'; import { ExperimentsProvider } from '../experiments'; +import { CourseInfoProvider } from '../context'; const Xpert = ({ courseId, contentToolsEnabled, unitId, - isUpgradeEligible, // eslint-disable-line no-unused-vars + isUpgradeEligible, }) => { const dispatch = useDispatch(); + const courseInfo = useMemo( + () => ({ courseId, unitId, isUpgradeEligible }), + [courseId, unitId, isUpgradeEligible], + ); const { isEnabled, @@ -40,22 +45,24 @@ const Xpert = ({ }; return isEnabled ? ( - - <> - - - - + + + <> + + + + + ) : null; };