Skip to content

Commit

Permalink
feat: Integrated Optimizely Prompt Experiment
Browse files Browse the repository at this point in the history
  • Loading branch information
rijuma committed Jun 27, 2024
1 parent c44db4a commit 2fb1862
Show file tree
Hide file tree
Showing 10 changed files with 73 additions and 6 deletions.
3 changes: 3 additions & 0 deletions src/components/Sidebar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import './Sidebar.scss';
import {
clearMessages,
} from '../../data/thunks';
import { PROMPT_EXPERIMENT_FLAG } from '../../constants/experiments';

const Sidebar = ({
courseId,
Expand All @@ -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();

Expand Down Expand Up @@ -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 } : {}),
});
};

Expand Down
9 changes: 7 additions & 2 deletions src/components/ToggleXpertButton/index.jsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -14,13 +14,15 @@ 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,
setIsOpen,
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);
Expand All @@ -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 } : {}),
},
);
}
Expand All @@ -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 } : {}),
});
};

Expand All @@ -63,6 +67,7 @@ const ToggleXpert = ({
course_id: courseId,
user_id: userId,
source: 'product-tour',
...(variationKey ? { experiment_name: PROMPT_EXPERIMENT_FLAG, variation_key: variationKey } : {}),
},
);
};
Expand All @@ -78,7 +83,7 @@ const ToggleXpert = ({
(!isOpen && (
<div
className={
`toggle position-fixed closed d-flex flex-column justify-content-end align-items-end mx-3 border-0
`toggle position-fixed closed d-flex flex-column justify-content-end align-items-end mx-3 border-0
${chatMargin}`
}
>
Expand Down
7 changes: 7 additions & 0 deletions src/constants/experiments.js
Original file line number Diff line number Diff line change
@@ -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,
};
10 changes: 7 additions & 3 deletions src/data/api.js
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down
Empty file removed src/data/index.js
Empty file.
10 changes: 10 additions & 0 deletions src/data/slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const learningAssistantSlice = createSlice({
disclosureAcknowledged: false,
sidebarIsOpen: false,
isEnabled: false,
experiments: {},
},
reducers: {
setCurrentMessage: (state, { payload }) => {
Expand Down Expand Up @@ -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];
},
},
});

Expand All @@ -61,6 +69,8 @@ export const {
setDisclosureAcknowledged,
setSidebarIsOpen,
setIsEnabled,
setExperiment,
clearExperiment,
} = learningAssistantSlice.actions;

export const {
Expand Down
16 changes: 15 additions & 1 deletion src/data/thunks.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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.
Expand All @@ -33,24 +37,34 @@ export function addChatMessage(role, content, courseId) {
dispatch(resetApiError());

const { userId } = getAuthenticatedUser();

sendTrackEvent('edx.ui.lms.learning_assistant.message', {
id: conversationId,
course_id: courseId,
user_id: userId,
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) {
Expand Down
20 changes: 20 additions & 0 deletions src/hooks/useOptimizelyExperiment.js
Original file line number Diff line number Diff line change
@@ -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;
Empty file removed src/utils/index.jsx
Empty file.
4 changes: 4 additions & 0 deletions src/widgets/Xpert.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -13,6 +15,8 @@ const Xpert = ({ courseId, contentToolsEnabled, unitId }) => {
sidebarIsOpen,
} = useSelector(state => state.learningAssistant);

useOptimizelyExperiment(PROMPT_EXPERIMENT_FLAG);

const setSidebarIsOpen = (isOpen) => {
dispatch(updateSidebarIsOpen(isOpen));
};
Expand Down

0 comments on commit 2fb1862

Please sign in to comment.