diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index 2d47f29..b7f924c 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -17,6 +17,7 @@ import './Sidebar.scss'; import { clearMessages, } from '../../data/thunks'; +import { PROMPT_EXPERIMENT_FLAG } from '../../constants/experiments'; const Sidebar = ({ courseId, @@ -29,6 +30,7 @@ const Sidebar = ({ disclosureAcknowledged, messageList, } = useSelector(state => state.learningAssistant); + const { variationKey } = useSelector(state => state.experiments?.[PROMPT_EXPERIMENT_FLAG]) || {}; const chatboxContainerRef = useRef(null); const dispatch = useDispatch(); @@ -80,6 +82,7 @@ const Sidebar = ({ dispatch(clearMessages()); sendTrackEvent('edx.ui.lms.learning_assistant.clear', { course_id: courseId, + ...(variationKey ? { experiment_name: PROMPT_EXPERIMENT_FLAG, variation_key: variationKey } : {}), }); }; diff --git a/src/components/ToggleXpertButton/index.jsx b/src/components/ToggleXpertButton/index.jsx index 6041c9b..f14f42e 100644 --- a/src/components/ToggleXpertButton/index.jsx +++ b/src/components/ToggleXpertButton/index.jsx @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React, { useState } from 'react'; - +import { useSelector } from 'react-redux'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { @@ -14,6 +14,7 @@ import { Close } from '@openedx/paragon/icons'; import { ReactComponent as XpertLogo } from '../../assets/xpert-logo.svg'; import './index.scss'; +import { PROMPT_EXPERIMENT_FLAG } from '../../constants/experiments'; const ToggleXpert = ({ isOpen, @@ -21,6 +22,7 @@ const ToggleXpert = ({ courseId, contentToolsEnabled, }) => { + const { variationKey } = useSelector(state => state.experiments?.[PROMPT_EXPERIMENT_FLAG]) || {}; const [hasDismissedCTA, setHasDismissedCTA] = useState(false); const [isModalOpen, setIsModalOpen] = useState(true); const [target, setTarget] = useState(null); @@ -35,6 +37,7 @@ const ToggleXpert = ({ course_id: courseId, user_id: userId, source: event.target.id === 'toggle-button' ? 'toggle' : 'cta', + ...(variationKey ? { experiment_name: PROMPT_EXPERIMENT_FLAG, variation_key: variationKey } : {}), }, ); } @@ -51,6 +54,7 @@ const ToggleXpert = ({ localStorage.setItem('dismissedLearningAssistantCallToAction', 'true'); sendTrackEvent('edx.ui.lms.learning_assistant.dismiss_action_message', { course_id: courseId, + ...(variationKey ? { experiment_name: PROMPT_EXPERIMENT_FLAG, variation_key: variationKey } : {}), }); }; @@ -63,6 +67,7 @@ const ToggleXpert = ({ course_id: courseId, user_id: userId, source: 'product-tour', + ...(variationKey ? { experiment_name: PROMPT_EXPERIMENT_FLAG, variation_key: variationKey } : {}), }, ); }; @@ -78,7 +83,7 @@ const ToggleXpert = ({ (!isOpen && (
diff --git a/src/constants/experiments.js b/src/constants/experiments.js new file mode 100644 index 0000000..f18efef --- /dev/null +++ b/src/constants/experiments.js @@ -0,0 +1,7 @@ +const PROMPT_EXPERIMENT_FLAG = '_cosmo__xpert_gpt_4_0_prompt'; +const PROMPT_EXPERIMENT_KEY = 'updated_prompt'; + +export { + PROMPT_EXPERIMENT_FLAG, + PROMPT_EXPERIMENT_KEY, +}; diff --git a/src/data/api.js b/src/data/api.js index ec10b41..7155dac 100644 --- a/src/data/api.js +++ b/src/data/api.js @@ -1,21 +1,25 @@ import { getConfig, snakeCaseObject } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -async function fetchChatResponse(courseId, messageList, unitId) { +async function fetchChatResponse(courseId, messageList, unitId, customQueryParams = {}) { const payload = messageList.map((message) => ({ role: message?.role, content: message?.content, })); - let queryParams = { unitId }; + let queryParams = { + unitId, + ...customQueryParams, + }; + queryParams = snakeCaseObject(queryParams); let queryString = new URLSearchParams(queryParams); queryString = queryString.toString(); const url = new URL(`${getConfig().CHAT_RESPONSE_URL}/${courseId}?${queryString}`); - const { data } = await getAuthenticatedHttpClient().post(url.href, payload); + return data; } diff --git a/src/data/index.js b/src/data/index.js deleted file mode 100644 index e69de29..0000000 diff --git a/src/data/slice.js b/src/data/slice.js index cf2be00..279a462 100644 --- a/src/data/slice.js +++ b/src/data/slice.js @@ -13,6 +13,7 @@ export const learningAssistantSlice = createSlice({ disclosureAcknowledged: false, sidebarIsOpen: false, isEnabled: false, + experiments: {}, }, reducers: { setCurrentMessage: (state, { payload }) => { @@ -47,6 +48,13 @@ export const learningAssistantSlice = createSlice({ setIsEnabled: (state, { payload }) => { state.isEnabled = payload; }, + setExperiment: (state, { payload }) => { + const { flag, decision } = payload; + state.experiments[flag] = decision; + }, + clearExperiment: (state, { payload: flag }) => { + delete state.experiments[flag]; + }, }, }); @@ -61,6 +69,8 @@ export const { setDisclosureAcknowledged, setSidebarIsOpen, setIsEnabled, + setExperiment, + clearExperiment, } = learningAssistantSlice.actions; export const { diff --git a/src/data/thunks.js b/src/data/thunks.js index bdc0a7f..ef10379 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -1,5 +1,7 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; + +import { trackChatBotMessageOptimizely } from '../utils/optimizelyExperiment'; import fetchChatResponse, { fetchLearningAssistantEnabled } from './api'; import { setCurrentMessage, @@ -13,10 +15,12 @@ import { setSidebarIsOpen, setIsEnabled, } from './slice'; +import { PROMPT_EXPERIMENT_FLAG } from '../constants/experiments'; export function addChatMessage(role, content, courseId) { return (dispatch, getState) => { const { messageList, conversationId } = getState().learningAssistant; + const { variationKey } = getState().experiments?.[PROMPT_EXPERIMENT_FLAG] || {}; // Redux recommends only serializable values in the store, so we'll stringify the timestap to store in Redux. // When we need to operate on the Date object, we'll deserialize the string. @@ -33,6 +37,7 @@ export function addChatMessage(role, content, courseId) { dispatch(resetApiError()); const { userId } = getAuthenticatedUser(); + sendTrackEvent('edx.ui.lms.learning_assistant.message', { id: conversationId, course_id: courseId, @@ -40,17 +45,26 @@ export function addChatMessage(role, content, courseId) { timestamp: message.timestamp, role: message.role, content: message.content, + ...(variationKey ? { experiment_name: PROMPT_EXPERIMENT_FLAG, variation_key: variationKey } : {}), }); }; } export function getChatResponse(courseId, unitId) { return async (dispatch, getState) => { + const { userId } = getAuthenticatedUser(); const { messageList } = getState().learningAssistant; + const { enabled, variationKey } = getState().experiments?.[PROMPT_EXPERIMENT_FLAG] || {}; + dispatch(setApiIsLoading(true)); try { - const message = await fetchChatResponse(courseId, messageList, unitId); + if (enabled) { + trackChatBotMessageOptimizely(userId); + } + const customQueryParams = variationKey ? { responseVariation: variationKey } : {}; + const message = await fetchChatResponse(courseId, messageList, unitId, customQueryParams); + dispatch(setApiIsLoading(false)); dispatch(addChatMessage(message.role, message.content, courseId)); } catch (error) { diff --git a/src/hooks/useOptimizelyExperiment.js b/src/hooks/useOptimizelyExperiment.js new file mode 100644 index 0000000..185b4eb --- /dev/null +++ b/src/hooks/useOptimizelyExperiment.js @@ -0,0 +1,20 @@ +import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { useDecision } from '@optimizely/react-sdk'; +import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; +import { setExperiment } from '../data/slice'; + +// We need this import to make sure Optimizely is instantiated. +import optimizelyInstance from '../data/optimizely'; // eslint-disable-line no-unused-vars + +const useOptimizelyExperiment = (flag) => { + const dispatch = useDispatch(); + const { userId } = getAuthenticatedUser(); + const [decision] = useDecision(flag, { autoUpdate: true }, { id: userId }); + + useEffect(() => { + dispatch(setExperiment({ flag, decision })); + }, [dispatch, flag, decision]); +}; + +export default useOptimizelyExperiment; diff --git a/src/utils/index.jsx b/src/utils/index.jsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/widgets/Xpert.jsx b/src/widgets/Xpert.jsx index 2634bc4..a59966b 100644 --- a/src/widgets/Xpert.jsx +++ b/src/widgets/Xpert.jsx @@ -4,6 +4,8 @@ import { useDispatch, useSelector } from 'react-redux'; import { updateSidebarIsOpen, getIsEnabled } from '../data/thunks'; import ToggleXpert from '../components/ToggleXpertButton'; import Sidebar from '../components/Sidebar'; +import useOptimizelyExperiment from '../hooks/useOptimizelyExperiment'; +import { PROMPT_EXPERIMENT_FLAG } from '../constants/experiments'; const Xpert = ({ courseId, contentToolsEnabled, unitId }) => { const dispatch = useDispatch(); @@ -13,6 +15,8 @@ const Xpert = ({ courseId, contentToolsEnabled, unitId }) => { sidebarIsOpen, } = useSelector(state => state.learningAssistant); + useOptimizelyExperiment(PROMPT_EXPERIMENT_FLAG); + const setSidebarIsOpen = (isOpen) => { dispatch(updateSidebarIsOpen(isOpen)); };