diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx
index 2d47f297..b7f924ce 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 6041c9bc..44aaba06 100644
--- a/src/components/ToggleXpertButton/index.jsx
+++ b/src/components/ToggleXpertButton/index.jsx
@@ -1,5 +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';
@@ -11,9 +12,9 @@ import {
ModalPopup,
} from '@openedx/paragon';
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 00000000..f18efefd
--- /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 ec10b41e..7155dac0 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 e69de29b..00000000
diff --git a/src/data/slice.js b/src/data/slice.js
index cf2be001..279a4628 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 bdc0a7f4..ef103798 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 00000000..185b4eb6
--- /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 e69de29b..00000000
diff --git a/src/widgets/Xpert.jsx b/src/widgets/Xpert.jsx
index 2634bc45..a59966b2 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));
};